How to Build a Complete REST API with Node.js, Express, and MongoDB
Building a REST API is the backbone of modern web development. Whether you are creating a backend for a mobile app, a single-page application, or a microservice, the pattern is essentially the same. This guide walks through building a complete REST API with Node.js, Express, and MongoDB that you could actually deploy to production.
I am going to assume you have Node.js installed and a basic understanding of JavaScript. If you have never used Express before, do not worry. Express is minimal and easy to pick up. The hardest part of building an API is not the code itself, it is making decisions about structure, error handling, and security. This guide gives you a template that works for most projects.
Project Setup and Structure
Create a new directory and initialize npm. I recommend organizing your project by feature, not by file type. Instead of having a controllers folder, a models folder, and a routes folder separately, group them by domain. For example, a user feature has user.routes.js, user.controller.js, and user.model.js all in a users folder. This scales better as your API grows.
Install the core dependencies: express, mongoose, dotenv, cors, and helmet. For development, install nodemon. Set up your package.json scripts: dev uses nodemon, start uses node directly. Create an index.js or server.js file as your entry point. This file should import Express, apply middleware, connect to MongoDB, and start listening on a port.
Connecting to MongoDB
Mongoose is the most popular MongoDB ODM for Node.js. It provides a schema-based solution to model your data. Create a config/db.js file that exports a function to connect to MongoDB using mongoose.connect(). Use your MongoDB connection string from an environment variable. Never hardcode connection strings. For local development, use MongoDB Community Edition or MongoDB Atlas free tier.
Handle connection events. Log when the connection is successful and when there is an error. Mongoose by default buffers commands when the connection is not yet established, which can mask issues. Set bufferCommands: false in your connection options if you want immediate feedback when the database is down. Also set serverSelectionTimeoutMS to a reasonable value like 5000 so your API does not hang indefinitely if MongoDB is unreachable.
Building Routes and Controllers
Define your routes using express.Router(). A typical CRUD resource looks like this: GET /api/items to list, GET /api/items/:id to get one, POST /api/items to create, PUT /api/items/:id to update, and DELETE /api/items/:id to delete. Keep your route files clean by only defining the HTTP method and path, and delegating the logic to controller functions.
Controllers handle the request-response cycle. They extract data from req.params, req.query, and req.body, call the appropriate service or model methods, and send the response. Controllers should not contain business logic directly. Instead, they orchestrate the flow. This separation makes testing easier because you can test business logic without making HTTP requests.
Input Validation with Joi or Zod
Never trust user input. Validate every request. Joi and Zod are the two most popular validation libraries. Zod is newer and integrates better with TypeScript. Define a schema for each endpoint that expects input. Create a validation middleware that takes a schema and validates req.body, req.params, or req.query. If validation fails, return a 400 status with a descriptive error message that tells the client exactly which field failed and why.
Authentication with JWT
JSON Web Tokens are the standard for API authentication. When a user registers or logs in, your API returns a signed JWT token. The client sends this token in the Authorization header for subsequent requests. Create an auth middleware that verifies the token and attaches the user to the request object. Store passwords securely using bcrypt with salt rounds of at least 10.
Error Handling Middleware
Create a custom error class that extends the built-in Error with a statusCode property. Throw these errors with appropriate status codes. Create a global error handling middleware with four parameters (err, req, res, next). In development, return the full error stack. In production, return a generic message and log the details. Create an asyncHandler wrapper that catches errors in async route handlers and passes them to the error middleware.
Pagination, Filtering, and Sorting
Implement offset-based pagination with page and limit query parameters. Return data along with total count, total pages, and next/previous page URLs. Allow clients to filter results by passing query parameters that map to MongoDB filters. For sorting, accept a sort parameter with field name and direction. Sanitize all query parameters to prevent query injection by stripping MongoDB operators like $gt and $ne.
File Uploads with Multer
Multer is the standard middleware for handling file uploads in Express. Configure it with a storage destination and filename strategy. Validate file types and sizes before saving. For production, use cloud storage like AWS S3 or Cloudinary and generate signed URLs. Set reasonable file size limits: 5MB for images, 50MB for videos.
Rate Limiting and Security
Use express-rate-limit to prevent abuse. Apply a global rate limit of 100 requests per 15 minutes per IP, and stricter limits on auth endpoints. Use helmet to set security-related HTTP headers. Use cors to restrict which domains can access your API. Enable HTTP request logging with morgan in development and winston in production.
Testing Your API
Write integration tests using Jest and Supertest. Set up a separate test database using MongoDB Memory Server. Write tests for happy path, validation errors, authentication failures, and edge cases. Aim for at least 80% coverage on your API routes. Automated tests catch regressions when you refactor.