This project contains a TypeScript and Express REST API that manages users and posts in PostgreSQL.
- TypeScript Express app with Zod v4 validation
- PostgreSQL 18 persistence via Drizzle ORM
- Native PostgreSQL
uuidv7()primary keys - JWT authentication for protected write routes
- User CRUD endpoints:
POST /usersGET /usersGET /users/:idPATCH /users/:idDELETE /users/:id
- Auth endpoint:
POST /auth/login
- Post CRUD endpoints:
POST /postsPUT /posts/:idGET /postsGET /posts/:idDELETE /posts/:id
- Jest test suite backed by an in-memory PostgreSQL-compatible database
usersstores the account record, password hash, and timestampspostsstores post records and referencesusers.iduser_favorite_foodsstores one favorite food per row, unique per user, with stable orderinguser_hobbiesstores one hobby per row, unique per user, with stable ordering and frequency per week
- Node.js:
24.14.0 - npm:
11.9.0 - Docker, if you want the local PostgreSQL helper container
- PostgreSQL 18, if you want to use an external database instead of Docker
npm installcp .env.example .envSupported variables:
PORT: HTTP port, defaults to3111DATABASE_URL: PostgreSQL connection stringDATABASE_SSL: Set totruefor TLS-enabled environments such as NeonDATABASE_SSL_REJECT_UNAUTHORIZED: Keeptrueunless you intentionally want relaxed certificate validationJWT_SECRET: JWT signing secret, minimum 32 charactersALLOWED_ORIGINS: A single origin or a comma-separated list of origins
cp docker/postgres.env.example docker/postgres.env
npm run postgres:startThe helper starts PostgreSQL 18 on port 5432 by default.
npm run db:migrateThis applies the generated Drizzle SQL migrations, including the PostgreSQL 18 uuidv7() defaults and indexes.
npm startSwagger UI is available at http://localhost:<PORT>/api-docs.
The generated OpenAPI JSON is available at http://localhost:<PORT>/openapi.json.
| Script | Description |
|---|---|
npm start |
Starts the server in development mode with nodemon. |
npm run build |
Compiles TypeScript and writes a generated OpenAPI spec to dist/openapi.json. |
npm run openapi:generate |
Writes a generated OpenAPI spec to openapi.json. |
npm run start:prod |
Starts the compiled server from dist/index.js. |
npm run postgres:start |
Starts the local PostgreSQL Docker container. |
npm run db:generate |
Generates a new Drizzle SQL migration from the schema. |
npm run db:migrate |
Applies generated Drizzle migrations to the database. |
npm run db:reset-local |
Drops and recreates the configured local development database. |
npm test |
Runs the Jest test suite. |
npm run type:check |
Runs TypeScript type checking. |
npm run format:check |
Checks code formatting with Prettier. |
npm run format:fix |
Fixes code formatting with Prettier. |
src/
index.ts # Process entrypoint, connects to PostgreSQL and starts HTTP server
app.ts # Express app setup, middleware, routes
env.ts # Environment variable loading and validation
openapi/ # Generated OpenAPI document and export script
auth/ # JWT and password helpers
constants-types/ # Shared constants, messages, and types
controllers/ # Route handlers
errors/ # Typed API errors
middleware/ # Auth, validation, and error handling middleware
routes/ # Express routers
db/
client.ts # Shared database connection, connection checks, and migration helpers
init.ts # Migration entrypoint used by `npm run db:migrate`
reset-local.ts # Local-only admin script that drops and recreates the configured database
schema.ts # Drizzle table definitions
test-utils.ts # Test-only helpers for clearing the in-memory database between Jest cases
models/ # API-facing data types
queries/ # Drizzle select/insert/update/delete operations
utils/ # UUID validation helper
drizzle/ # Generated Drizzle SQL migrations and metadata
drizzle.config.ts # Drizzle migration configuration
test/
helpers/ # Shared test helpers
setup/ # Jest environment bootstrap files
docker/
postgres.env.example
scripts/
run-postgres.sh
- Route shapes, response shapes, validation rules, auth flow, and ownership checks remain the same
- Users still embed
favoriteFoods,hobbies, andpostsin API responses - User and post lookups validate UUIDs
- Favorite foods and hobbies are stored through lookup tables with globally unique names while preserving each user's input order
- Duplicate favorite food names are deduplicated by exact value while keeping the first occurrence
- Duplicate hobby names are deduplicated by name while keeping the first occurrence, including that first hobby's
frequencyPerWeekvalue
The OpenAPI document is generated from the route-layer Zod schemas and OpenAPI registry in src/openapi/.
npm run openapi:generatewrites the current spec toopenapi.jsonnpm run buildwrites the compiled app and a generated spec todist/openapi.jsonGET /openapi.jsonreturns the live generated document used by Swagger UI
src/routes/*.tscontains the Zod request schemas that define validation rules such as required fields, enums, array shapes, and min/max constraints.src/openapi/document.tsreuses those schemas and registers the public API contract for each route, including path, method, tags, auth requirements, and documented responses.src/openapi/write-openapi.tsserializes the generated OpenAPI document to JSON when you runnpm run openapi:generateornpm run build.
- Changes to reused Zod schemas in the route layer usually flow into the generated OpenAPI schemas automatically.
- Examples include field type changes, new required properties, enum updates, nested object changes, and validation constraints such as
.min()or.max().
- New endpoints still need a matching
registry.registerPath(...)entry insrc/openapi/document.ts. - Route metadata changes such as path, method, summary, tags, security requirements, response codes, and response body documentation must also be updated in
src/openapi/document.ts. - If an endpoint stops being exposed publicly, remove both the runtime route and its OpenAPI registration.
- Update the API code and Zod schemas in the route layer.
- Update
src/openapi/document.tsif you changed route behavior, responses, auth, or added a new endpoint. - Run
npm run type:check. - Run
npm run openapi:generateif you want a local generated spec artifact. - Run
npm run buildto verify the production build also emitsdist/openapi.json.
In short, the current setup keeps request and schema details close to the route validation code, but src/openapi/document.ts remains the place where the public API surface is described.
The test suite uses Jest with an in-memory PostgreSQL-compatible backend so the repository does not need Docker just to run tests. Test cleanup is separate from local development reset behavior:
src/db/test-utils.tstruncates tables inside the in-memory test database between test cases.src/db/reset-local.tsdrops and recreates a real local PostgreSQL database, and refuses to run against non-local hosts.
npm test- Update the schema in
src/db/schema.ts. - Generate a new migration with
npm run db:generate. - Apply migrations locally with
npm run db:migrate. - Run
npm testandnpm run type:check.
Never hand-edit files in drizzle/. Treat them as generated artifacts and regenerate them from the schema whenever the model changes.
To recreate your local development database from scratch, run npm run db:reset-local and then npm run db:migrate. The reset command only runs when DATABASE_URL points to a local PostgreSQL host such as localhost or 127.0.0.1.
src/db/client.ts no longer contains data-reset helpers because row-level cleanup is only needed in tests. Its responsibilities are now limited to opening the active database, verifying connectivity, applying migrations, and closing the database cleanly.
- Runtime: Node.js
- Language: TypeScript
- Framework: Express.js
- Database: PostgreSQL 18
- ORM: Drizzle ORM
- Validation: Zod
- Testing: Jest, Supertest, PGlite
- Tooling: ESLint, Prettier, Nodemon
- Local development: PostgreSQL 18 Docker container
- Production database: Neon PostgreSQL 18
- Production compute: Amazon ECS on AWS Fargate
- Container registry: Amazon ECR
See deployment.md for the production deployment workflow.