Skip to content

ryanaxondev/express-book-management

Repository files navigation

📚 Bookstore API (v3)

A Node.js + TypeScript REST API for managing books and categories, powered by Express, Drizzle ORM, and PostgreSQL.

Now fully migrated to use UUID-based identifiers instead of numeric IDs, improving data consistency and scalability across distributed systems.


⚡ Quick Start

git clone https://github.com/ryanaxondev/express-book-management.git
cd express-book-management
npm install
cp .env.example .env
# Update .env values if needed
docker compose up -d
npm run db:push
npm run dev

The API will run at http://localhost:3000 by default.


🧱 Tech Stack

  • Node.js + TypeScript
  • Express.js
  • Drizzle ORM
  • PostgreSQL (Docker)
  • Docker Compose
  • Zod (Validation)

🗂 Project Structure

bookstore/
│── src/
│   ├── app.ts                     # Express app configuration
│   ├── routes/
│   │   ├── books.ts               # Book routes
│   │   └── categoryRoutes.ts      # Category routes
│   ├── controllers/
│   │   ├── bookController.ts      # Book logic (with category support)
│   │   └── categoryController.ts  # Category CRUD logic
│   ├── validation/
│   │   ├── bookSchema.ts          # Zod schemas for book validation
│   │   └── categorySchema.ts      # Zod schemas for category validation
│   ├── utils/
│   │   └── mapToBookWithCategory.ts # Map joined DB rows to BookWithCategory
│   └── db/
│       ├── index.ts               # Drizzle + PostgreSQL connection
│       └── schema.ts              # Database schema (Books + Categories)
│
│── docs/
│   └── postman/
│       ├── Bookstore_API_Pro_Collection.json   # Postman collection for all endpoints
│       └── Bookstore_API_Environment.json      # Postman environment configuration
│
│── server.ts                      # Entry point
│── package.json
│── tsconfig.json
│── drizzle.config.ts
│── docker-compose.yml
│── .env
│── .env.example
│── .gitignore
│── CHANGELOG.md

⚙️ Environment Variables

DATABASE_URL=postgres://postgres:mysecretpassword@localhost:5433/bookstore
PORT=3000

Keep host/port consistent with docker-compose.yml when using Docker.


🛣️ Docker Setup

docker compose up -d

Starts a PostgreSQL container on port 5433.

Verify connection:

psql -h localhost -p 5433 -U postgres -d bookstore

🧱 Database (Drizzle ORM + UUID)

UUID Integration

All tables now use UUID instead of serial numeric IDs. UUIDs are automatically generated by PostgreSQL using gen_random_uuid().

id: uuid('id').defaultRandom().primaryKey(),

Apply migrations:

npm run db:push

Generate new migrations:

npm run db:generate

🚀 Running the API

Development

npm run dev

Production

npm run build
npm start

📚 API Endpoints (UUID Version)

💘 Books

Method Endpoint Description Request Body Example
GET /books Get all books
GET /books/:id Get a single book by ID
POST /books Add a new book { "title": "Book A", "author": "John Doe", "description": "A short description", "categoryId": "uuid-here" }
PUT /books/:id Update book details { "title": "Updated", "author": "Jane Doe", "categoryId": "uuid-here" }
DELETE /books/:id Delete a book by ID

Example Response:

{
  "id": "bf8f752c-8e83-49eb-8afc-163b0573ddda",
  "title": "1984",
  "author": "George Orwell",
  "description": "A dystopian novel",
  "categoryId": "c03f49ca-343e-4be0-8f5f-74b9bfe0da5e",
  "category": {
    "id": "c03f49ca-343e-4be0-8f5f-74b9bfe0da5e",
    "name": "Fiction",
    "description": "Narrative works"
  }
}

🏷️ Categories

Method Endpoint Description Request Body Example
GET /categories Get all categories
GET /categories/:id Get category by ID
POST /categories Create a new category { "name": "Science", "description": "Books about physics" }
PUT /categories/:id Update a category { "name": "Tech", "description": "Updated description" }
DELETE /categories/:id Delete a category

Deleting a category will not delete related books; their categoryId becomes NULL.


✅ Validation & Error Handling (Zod)

  • All POST and PUT requests for Books and Categories are validated using Zod schemas.

  • Includes strict UUID validation via:

    const uuidSchema = z.string().uuid();
  • Validation errors return structured, human-readable JSON responses:

    {
      "error": {
        "categoryId": "Invalid UUID format"
      }
    }

💪 TypeScript Models

export type UUID = string & { readonly brand: unique symbol };

export type Category = {
  id: UUID;
  name: string;
  description?: string | null;
};

export type BookInput = {
  title: string;
  author: string;
  description?: string | null;
  categoryId?: UUID | null;
};

export type BookWithCategory = BookInput & {
  id: UUID;
  category?: Category | null;
};

🤖 Utility: mapToBookWithCategory

import { BookWithCategory, UUID } from "../types/bookTypes.js";

export const mapToBookWithCategory = (row: any): BookWithCategory => ({
  id: row.id as UUID,
  title: row.title,
  author: row.author,
  description: row.description ?? null,
  categoryId: row.categoryId ?? null,
  category: row.categories_id
    ? {
        id: row.categories_id as UUID,
        name: row.categories_name,
        description: row.categories_description ?? null,
      }
    : null,
});

👩‍💻 Postman Setup

Use the updated Postman collection and environment under /docs/postman.

Example Environment:

{
  "base_url": "http://localhost:3000",
  "book_id": "bf8f752c-8e83-49eb-8afc-163b0573ddda",
  "category_id": "c03f49ca-343e-4be0-8f5f-74b9bfe0da5e"
}

🕹️ Testing

curl -X POST http://localhost:3000/categories \
  -H "Content-Type: application/json" \
  -d '{"name": "Fiction", "description": "Narrative works"}'

All CRUD routes were verified using curl and Postman with UUID-based data.


🛠️ npm Scripts

{
  "dev": "tsx watch server.ts",
  "build": "tsc",
  "start": "node dist/server.js",
  "db:generate": "drizzle-kit generate:pg",
  "db:push": "drizzle-kit push:pg"
}

📈 Summary of v3 (UUID Migration)

  • All numeric IDs replaced with UUID types.
  • New UUID type introduced in bookTypes.ts.
  • Updated all controllers, routes, and validation schemas to handle UUIDs.
  • Added mapToBookWithCategory util for joined query mapping.
  • Updated Postman collection and environment with UUID examples.
  • Verified CRUD operations end-to-end with real data.

💼 License

This project is licensed under the MIT License.


💎 Part of the AXON Open Source Ecosystem

This project is part of AXON, a collection of open-source tools and libraries designed for high-quality, scalable web development.

About

A clean and minimal Node.js + Express REST API for managing a collection of books. It provides full CRUD functionality (Create, Read, Update, Delete) and is designed with scalability in mind — easily extendable with PostgreSQL and Drizzle ORM for type-safe, persistent data storage.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors