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.
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 devThe API will run at http://localhost:3000 by default.
- Node.js + TypeScript
- Express.js
- Drizzle ORM
- PostgreSQL (Docker)
- Docker Compose
- Zod (Validation)
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
DATABASE_URL=postgres://postgres:mysecretpassword@localhost:5433/bookstore
PORT=3000Keep host/port consistent with
docker-compose.ymlwhen using Docker.
docker compose up -dStarts a PostgreSQL container on port
5433.
Verify connection:
psql -h localhost -p 5433 -U postgres -d bookstoreAll 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:pushGenerate new migrations:
npm run db:generatenpm run devnpm run build
npm start| 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"
}
}| 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
categoryIdbecomesNULL.
-
All
POSTandPUTrequests 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" } }
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;
};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,
});Use the updated Postman collection and environment under /docs/postman.
{
"base_url": "http://localhost:3000",
"book_id": "bf8f752c-8e83-49eb-8afc-163b0573ddda",
"category_id": "c03f49ca-343e-4be0-8f5f-74b9bfe0da5e"
}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.
{
"dev": "tsx watch server.ts",
"build": "tsc",
"start": "node dist/server.js",
"db:generate": "drizzle-kit generate:pg",
"db:push": "drizzle-kit push:pg"
}- All numeric IDs replaced with UUID types.
- New
UUIDtype introduced inbookTypes.ts. - Updated all controllers, routes, and validation schemas to handle UUIDs.
- Added
mapToBookWithCategoryutil for joined query mapping. - Updated Postman collection and environment with UUID examples.
- Verified CRUD operations end-to-end with real data.
This project is licensed under the MIT License.
This project is part of AXON, a collection of open-source tools and libraries designed for high-quality, scalable web development.