Skip to content

abdusabri/users-posts-node

Repository files navigation

Users & Posts REST API – TypeScript, Express, MongoDB

This project contains a TypeScript/Express REST API that manages users and posts in MongoDB.

Overview

  • 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 password
    • GET /users – list users with optional creation-date sorting and text search
    • GET /users/:id – fetch a single user (includes virtual posts)
    • PATCH /users/:id – update your own profile fields
    • DELETE /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 user
    • PUT /posts/:id – update your own post
    • GET /posts – list all posts
    • GET /posts/:id – fetch a single post
    • DELETE /posts/:id – delete your own post
  • Jest test suite with in-memory MongoDB

Prerequisites

  • Node.js: 24.14.0 (see package.json engines) - (Recommended to use nvm and .nvmrc)
  • npm: 11.9.0 (see package.json engines)
  • Docker (optional, for local MongoDB via helper script)

Quick Start

Follow these steps to get the application running locally:

1. Install Dependencies

npm install

2. Environment Setup

Configuration 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 .env

The 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 *

3. Start Local Database

This project uses Docker to run a local MongoDB instance.

First, set up the Docker environment variables:

cp docker/mongodb.env.example docker/mongodb.env

Then, start the MongoDB container:

npm run mongo:start

Note: This script runs a MongoDB container on port 27017 with default credentials (admin/password) - if using the default env vars from the example file.

Note: If you already have a MongoDB instance, set MONGODB_URI accordingly in .env (for example, a MongoDB Atlas URI).

4. Run the Application

Start the development server with hot-reloading:

npm start

5. Access API Documentation

Once 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.

Scripts

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.

Project Structure

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.tssrc/routes/user-routes.ts or src/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 by db/models/
  • Users have a virtual posts relationship (auto-populated on queries)
  • JWT-protected write routes use the authenticated user id from the bearer token
  • Errors centralized via error-handler.ts and errors/api-errors.ts

Testing

The project uses Jest, and mongodb-memory-server for fast, isolated tests.

To run the tests:

npm test

Tests are located in the test/ directory.

Tech Stack

  • Runtime: Node.js
  • Language: TypeScript
  • Framework: Express.js
  • Database: MongoDB (with Mongoose)
  • Validation: Zod
  • Testing: Jest, Supertest
  • Tooling: ESLint, Prettier, Nodemon

API Overview

Base URL (local):

  • http://localhost:<PORT> (default http://localhost:3111)

POST /users

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 characters
  • email: required, valid email format, lowercased, unique across users
  • password: required, length between 8 and 72 characters, stored as a hash
  • favoriteFoods (optional): array of strings, each trimmed, length between 3 and 50 characters
  • hobbies (optional): array of objects, each with:
    • name: required, string, trimmed, length between 3 and 100 characters
    • frequencyPerWeek: required, positive integer (minimum 1)

Responses:

  • 201 Created with 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 Request with validation details when payload is invalid

  • 409 Conflict when the email already exists

POST /auth/login

Authenticate a user and return a bearer token.

Request body:

{
    "email": "jane@example.com",
    "password": "password123"
}

Responses:

  • 200 OK with a JSON body containing a token
  • 400 Bad Request when validation fails
  • 401 Unauthorized when the credentials are invalid

Protected write routes

The following routes require an Authorization: Bearer <token> header:

  • PATCH /users/:id
  • DELETE /users/:id
  • POST /posts
  • PUT /posts/:id
  • DELETE /posts/:id

GET /users

List users, optionally sorted by creation date and/or filtered by text search.

Query parameters:

  • created (optional):
    • 'asc' – sort by createdAt ascending (oldest first)
    • 'desc' – sort by createdAt descending (newest first)
  • search (optional): text search across name and email fields (case-insensitive)

Examples:

  • GET /users – unsorted list (default MongoDB behavior)
  • GET /users?created=asc – ascending creation date
  • GET /users?created=desc – descending creation date
  • GET /users?search=jane – search users by name or email
  • GET /users?created=asc&search=jane – combined sort and search

Responses:

  • 200 OK with JSON array of users (may be empty). Each user includes their virtual posts array.

GET /users/:id

Fetch a single user by id.

Path parameters:

  • id: MongoDB ObjectId

Responses:

  • 200 OK with user JSON when found
  • 400 Bad Request when id is not a valid ObjectId
  • 404 Not Found when no user exists for the given id

PATCH /users/:id

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, or hobbies is required
  • name (optional): string, trimmed, length between 3 and 100 characters
  • favoriteFoods (optional): array of strings, each trimmed, length between 3 and 50 characters
  • hobbies (optional): array of objects, each with:
    • name: required, string, trimmed, length between 3 and 100 characters
    • frequencyPerWeek: required, positive integer (minimum 1)

Responses:

  • 200 OK with the updated user JSON
  • 400 Bad Request when id is not a valid ObjectId or validation fails
  • 401 Unauthorized when the bearer token is missing or invalid
  • 403 Forbidden when the authenticated user does not own the target account
  • 404 Not Found when no user exists for the given id

DELETE /users/:id

Delete your own user account.

Path parameters:

  • id: MongoDB ObjectId

Responses:

  • 204 No Content on successful deletion
  • 400 Bad Request when id is not a valid ObjectId
  • 401 Unauthorized when the bearer token is missing or invalid
  • 403 Forbidden when the authenticated user does not own the target account
  • 404 Not Found when no user exists for the given id
  • Deleting a user also deletes all posts authored by that user

POST /posts

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 posts
  • content: required, string, minimum 1 character
  • The author is derived from the JWT, not the request body

Responses:

  • 201 Created with 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 Request with validation details when payload is invalid

  • 401 Unauthorized when the bearer token is missing or invalid

  • 404 Not Found when the referenced author does not exist

  • 409 Conflict when a post with the same title already exists

GET /posts

List all posts.

Responses:

  • 200 OK with JSON array of posts (may be empty)

GET /posts/:id

Fetch a single post by id.

Path parameters:

  • id: MongoDB ObjectId

Responses:

  • 200 OK with post JSON when found
  • 400 Bad Request when id is not a valid ObjectId
  • 404 Not Found when no post exists for the given id

PUT /posts/:id

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 posts
  • content: required, string, minimum 1 character

Responses:

  • 200 OK with the updated post JSON
  • 400 Bad Request when id is not a valid ObjectId or validation fails
  • 401 Unauthorized when the bearer token is missing or invalid
  • 403 Forbidden when the authenticated user does not own the post
  • 404 Not Found when no post exists for the given id
  • 409 Conflict when another post already uses the requested title

DELETE /posts/:id

Delete your own post.

Path parameters:

  • id: MongoDB ObjectId

Responses:

  • 204 No Content on successful deletion
  • 400 Bad Request when id is not a valid ObjectId
  • 401 Unauthorized when the bearer token is missing or invalid
  • 403 Forbidden when the authenticated user does not own the post
  • 404 Not Found when no post exists for the given id

Error Handling Model

  • All validation errors from Zod are normalized into a 400 response with shape:

    {
        "error": "Validation failed",
        "details": [{ "path": "body.name", "message": "Name is too short" }]
    }
  • Application-specific errors are thrown using subclasses of APIError in errors/api-errors.ts

  • Unknown errors are converted into a 500 Internal Server Error by error-handler.ts

Additional Notes

  • Security headers are added using helmet.
  • Mongoose models use timestamps: true and customize toJSON to expose id instead of _id.
  • The User model has a virtual posts field that auto-populates associated posts on find and findOne queries.
  • A text index on name and email in the User model enables the search query parameter on GET /users.
  • Posts reference their author via an author field (MongoDB ObjectId), creating a one-to-many relationship.

About

Users & Posts REST API – TypeScript, Express, MongoDB

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages