This project contains a TypeScript/Express REST API that manages users and posts in MongoDB.
- TypeScript Express app with Zod v4 validation
- MongoDB persistence via Mongoose
- JWT authentication for protected write routes
- User CRUD endpoints:
POST /users– create a user account with a passwordGET /users– list users with optional creation-date sorting and text searchGET /users/:id– fetch a single user (includes virtual posts)PATCH /users/:id– update your own profile fieldsDELETE /users/:id– delete your own user account
- Auth endpoint:
POST /auth/login– exchange email/password for a JWT
- Post CRUD endpoints:
POST /posts– create a post as the authenticated userPUT /posts/:id– update your own postGET /posts– list all postsGET /posts/:id– fetch a single postDELETE /posts/:id– delete your own post
- Jest test suite with in-memory MongoDB
- Node.js:
24.14.0(seepackage.jsonengines) - (Recommended to usenvmand.nvmrc) - npm:
11.9.0(seepackage.jsonengines) - Docker (optional, for local MongoDB via helper script)
Follow these steps to get the application running locally:
npm installConfiguration is loaded through src/env.ts using dotenv and validated with Zod before the app boots.
Create a .env file in the root directory based on the example:
cp .env.example .envThe following variables are supported:
PORT: The port the server will listen on (default:3111).MONGODB_URI: Connection string for MongoDB.JWT_SECRET: Secret used to sign and verify JWTs. Use at least 32 characters.ALLOWED_ORIGINS: A single value, or comma-separated list of allowed CORS origins. For dev, you can set it to*
This project uses Docker to run a local MongoDB instance.
First, set up the Docker environment variables:
cp docker/mongodb.env.example docker/mongodb.envThen, start the MongoDB container:
npm run mongo:startNote: This script runs a MongoDB container on port
27017with default credentials (admin/password) - if using the default env vars from the example file.
Note: If you already have a MongoDB instance, set
MONGODB_URIaccordingly in.env(for example, a MongoDB Atlas URI).
Start the development server with hot-reloading:
npm startOnce the server is running, you can access the Swagger UI documentation at:
http://localhost:<YOUR_PORT>/api-docs
This interface allows you to explore the API endpoints and test them manually.
| Script | Description |
|---|---|
npm start |
Starts the server in development mode with nodemon. |
npm run start:prod |
Starts the server in production-like mode using ts-node directly. |
npm test |
Runs the test suite using jest. |
npm run mongo:start |
Starts the local MongoDB Docker container. |
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 MongoDB and starts HTTP server
app.ts # Express app setup, middleware, routes
env.ts # Environment variable loading and Zod validation
swagger.json # OpenAPI/Swagger specification
routes/
auth-routes.ts # Express Router for /auth endpoints
user-routes.ts # Express Router for /users endpoints
post-routes.ts # Express Router for /posts endpoints
controllers/
auth-controller.ts # Route handlers for authentication
user-controller.ts # Route handlers for user operations
post-controller.ts # Route handlers for post operations
auth/
jwt.ts # JWT signing and verification helpers
password.ts # Password hashing and verification helpers
db/
models/
user-model.ts # Mongoose User schema and model (with hobbies, favoriteFoods, virtual posts)
post-model.ts # Mongoose Post schema and model
queries/
user-queries.ts # Query helpers for user operations
post-queries.ts # Query helpers for post operations
utils/
id-validation.ts # ObjectId validation helper
middleware/
authenticate-request.ts # Bearer token authentication middleware
validate-request.ts # Zod-based request validation wrapper
error-handler.ts # Central error handling middleware
errors/
api-errors.ts # Typed API error classes
constants-types/
sort-order.ts # Sort order constants and type guard
test/
setup/ # Jest global setup/teardown and per-test setup
user-routes.test.ts # Integration tests for /users endpoints
post-routes.test.ts # Integration tests for /posts endpoints
index.test.ts # Catch-all 404 test
docker/
mongodb.env.example # Example Docker MongoDB env vars
scripts/
run-mongodb.sh # Helper to start local MongoDB via Docker
Key flows:
- HTTP request →
src/app.ts→src/routes/user-routes.tsorsrc/routes/post-routes.ts - Request validation with Zod via
validate-request - Business logic in
user-controller.ts/post-controller.ts - Persistence in
db/queries/backed bydb/models/ - Users have a virtual
postsrelationship (auto-populated on queries) - JWT-protected write routes use the authenticated user id from the bearer token
- Errors centralized via
error-handler.tsanderrors/api-errors.ts
The project uses Jest, and mongodb-memory-server for fast, isolated tests.
To run the tests:
npm testTests are located in the test/ directory.
- Runtime: Node.js
- Language: TypeScript
- Framework: Express.js
- Database: MongoDB (with Mongoose)
- Validation: Zod
- Testing: Jest, Supertest
- Tooling: ESLint, Prettier, Nodemon
Base URL (local):
http://localhost:<PORT>(defaulthttp://localhost:3111)
Create a new user.
Request body:
{
"name": "Jane Doe",
"email": "jane@example.com",
"password": "password123",
"favoriteFoods": ["Pizza", "Pasta"],
"hobbies": [
{
"name": "Running",
"frequencyPerWeek": 4
}
]
}Constraints (enforced via Zod and Mongoose):
name: required, string, trimmed, length between 3 and 100 charactersemail: required, valid email format, lowercased, unique across userspassword: required, length between 8 and 72 characters, stored as a hashfavoriteFoods(optional): array of strings, each trimmed, length between 3 and 50 charactershobbies(optional): array of objects, each with:name: required, string, trimmed, length between 3 and 100 charactersfrequencyPerWeek: required, positive integer (minimum 1)
Responses:
-
201 Createdwith JSON body containing the created user:{ "id": "<mongo-object-id>", "name": "Jane Doe", "email": "jane@example.com", "favoriteFoods": ["Pizza", "Pasta"], "hobbies": [ { "name": "Running", "frequencyPerWeek": 4 } ], "posts": [], "createdAt": "2025-01-01T00:00:00.000Z", "updatedAt": "2025-01-01T00:00:00.000Z" } -
400 Bad Requestwith validation details when payload is invalid -
409 Conflictwhen the email already exists
Authenticate a user and return a bearer token.
Request body:
{
"email": "jane@example.com",
"password": "password123"
}Responses:
200 OKwith a JSON body containing atoken400 Bad Requestwhen validation fails401 Unauthorizedwhen the credentials are invalid
The following routes require an Authorization: Bearer <token> header:
PATCH /users/:idDELETE /users/:idPOST /postsPUT /posts/:idDELETE /posts/:id
List users, optionally sorted by creation date and/or filtered by text search.
Query parameters:
created(optional):'asc'– sort bycreatedAtascending (oldest first)'desc'– sort bycreatedAtdescending (newest first)
search(optional): text search acrossnameandemailfields (case-insensitive)
Examples:
GET /users– unsorted list (default MongoDB behavior)GET /users?created=asc– ascending creation dateGET /users?created=desc– descending creation dateGET /users?search=jane– search users by name or emailGET /users?created=asc&search=jane– combined sort and search
Responses:
200 OKwith JSON array of users (may be empty). Each user includes their virtualpostsarray.
Fetch a single user by id.
Path parameters:
id: MongoDB ObjectId
Responses:
200 OKwith user JSON when found400 Bad Requestwhenidis not a valid ObjectId404 Not Foundwhen no user exists for the givenid
Update your own user profile fields.
Path parameters:
id: MongoDB ObjectId
Request body:
{
"name": "Updated Name",
"favoriteFoods": ["Sushi", "Tacos"],
"hobbies": [
{
"name": "Cycling",
"frequencyPerWeek": 3
}
]
}Constraints:
- At least one of
name,favoriteFoods, orhobbiesis required name(optional): string, trimmed, length between 3 and 100 charactersfavoriteFoods(optional): array of strings, each trimmed, length between 3 and 50 charactershobbies(optional): array of objects, each with:name: required, string, trimmed, length between 3 and 100 charactersfrequencyPerWeek: required, positive integer (minimum 1)
Responses:
200 OKwith the updated user JSON400 Bad Requestwhenidis not a valid ObjectId or validation fails401 Unauthorizedwhen the bearer token is missing or invalid403 Forbiddenwhen the authenticated user does not own the target account404 Not Foundwhen no user exists for the givenid
Delete your own user account.
Path parameters:
id: MongoDB ObjectId
Responses:
204 No Contenton successful deletion400 Bad Requestwhenidis not a valid ObjectId401 Unauthorizedwhen the bearer token is missing or invalid403 Forbiddenwhen the authenticated user does not own the target account404 Not Foundwhen no user exists for the givenid- Deleting a user also deletes all posts authored by that user
Create a new post as the authenticated user.
Request body:
{
"title": "My First Post",
"content": "This is the content of the post."
}Constraints (enforced via Zod and Mongoose):
title: required, string, trimmed, length between 3 and 200 characters, unique across postscontent: required, string, minimum 1 character- The
authoris derived from the JWT, not the request body
Responses:
-
201 Createdwith JSON body containing the created post:{ "id": "<mongo-object-id>", "title": "My First Post", "content": "This is the content of the post.", "author": "<author-object-id>", "createdAt": "2025-01-01T00:00:00.000Z", "updatedAt": "2025-01-01T00:00:00.000Z" } -
400 Bad Requestwith validation details when payload is invalid -
401 Unauthorizedwhen the bearer token is missing or invalid -
404 Not Foundwhen the referenced author does not exist -
409 Conflictwhen a post with the same title already exists
List all posts.
Responses:
200 OKwith JSON array of posts (may be empty)
Fetch a single post by id.
Path parameters:
id: MongoDB ObjectId
Responses:
200 OKwith post JSON when found400 Bad Requestwhenidis not a valid ObjectId404 Not Foundwhen no post exists for the givenid
Update your own post.
Path parameters:
id: MongoDB ObjectId
Request body:
{
"title": "Updated Post Title",
"content": "This is the updated post content."
}Constraints:
title: required, string, trimmed, length between 3 and 200 characters, unique across postscontent: required, string, minimum 1 character
Responses:
200 OKwith the updated post JSON400 Bad Requestwhenidis not a valid ObjectId or validation fails401 Unauthorizedwhen the bearer token is missing or invalid403 Forbiddenwhen the authenticated user does not own the post404 Not Foundwhen no post exists for the givenid409 Conflictwhen another post already uses the requested title
Delete your own post.
Path parameters:
id: MongoDB ObjectId
Responses:
204 No Contenton successful deletion400 Bad Requestwhenidis not a valid ObjectId401 Unauthorizedwhen the bearer token is missing or invalid403 Forbiddenwhen the authenticated user does not own the post404 Not Foundwhen no post exists for the givenid
-
All validation errors from Zod are normalized into a
400response with shape:{ "error": "Validation failed", "details": [{ "path": "body.name", "message": "Name is too short" }] } -
Application-specific errors are thrown using subclasses of
APIErrorinerrors/api-errors.ts -
Unknown errors are converted into a
500 Internal Server Errorbyerror-handler.ts
- Security headers are added using
helmet. - Mongoose models use
timestamps: trueand customizetoJSONto exposeidinstead of_id. - The
Usermodel has a virtualpostsfield that auto-populates associated posts onfindandfindOnequeries. - A text index on
nameandemailin the User model enables thesearchquery parameter onGET /users. - Posts reference their author via an
authorfield (MongoDB ObjectId), creating a one-to-many relationship.