Writing REST APIs With Express Router: A Complete Walkthrough
A complete walkthrough of building a REST API with Express Router.
Writing REST APIs With Express Router: A Complete Walkthrough
A complete walkthrough of building a REST API with Express Router. We will build a posts API.
Step 1: Set Up the Schema
// models/post.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: { type: String, required: true, minlength: 3, maxlength: 200 },
body: { type: String, required: true },
authorId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
tags: [String],
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
}, { timestamps: true });
postSchema.index({ authorId: 1, createdAt: -1 });
module.exports = mongoose.model('Post', postSchema);
Step 2: Write the Controller
// controllers/postController.js
const Post = require('../models/post');
const ApiError = require('../utils/ApiError');
const asyncHandler = require('../utils/asyncHandler');
exports.listPosts = asyncHandler(async (req, res) => {
const skip = parseInt(req.query.skip) || 0;
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
const filter = { authorId: req.user.userId };
if (req.query.tag) filter.tags = req.query.tag;
const posts = await Post.find(filter).sort({ createdAt: -1 }).skip(skip).limit(limit).lean();
const total = await Post.countDocuments(filter);
res.json({ data: posts, total, skip, limit });
});
exports.createPost = asyncHandler(async (req, res) => {
const post = await Post.create({ ...req.body, authorId: req.user.userId });
res.status(201).json(post);
});
exports.getPost = asyncHandler(async (req, res) => {
const post = await Post.findById(req.params.id).populate('authorId', 'firstName lastName');
if (!post) throw new ApiError(404, 'Post not found');
res.json(post);
});
exports.updatePost = asyncHandler(async (req, res) => {
const post = await Post.findById(req.params.id);
if (!post) throw new ApiError(404, 'Post not found');
if (post.authorId.toString() !== req.user.userId) throw new ApiError(403, 'Not authorized');
Object.assign(post, req.body);
await post.save();
res.json(post);
});
exports.deletePost = asyncHandler(async (req, res) => {
const post = await Post.findById(req.params.id);
if (!post) throw new ApiError(404, 'Post not found');
if (post.authorId.toString() !== req.user.userId) throw new ApiError(403, 'Not authorized');
await post.deleteOne();
res.status(204).end();
});
Step 3: Write the Router
// routes/posts.js
const express = require('express');
const router = express.Router();
const auth = require('../middlewares/auth');
const validate = require('../middlewares/validate');
const { createPostSchema, updatePostSchema } = require('../validations/post');
const controller = require('../controllers/postController');
router.use(auth);
router.get('/', controller.listPosts);
router.post('/', validate(createPostSchema), controller.createPost);
router.get('/:id', controller.getPost);
router.patch('/:id', validate(updatePostSchema), controller.updatePost);
router.delete('/:id', controller.deletePost);
module.exports = router;
Step 4: Mount the Router
// app.js
app.use('/api/v1/posts', require('./routes/posts'));
Step 5: Write the Validation Schema
// validations/post.js
const { z } = require('zod');
const createPostSchema = z.object({
title: z.string().min(3).max(200),
body: z.string().min(1),
tags: z.array(z.string()).optional()
});
const updatePostSchema = z.object({
title: z.string().min(3).max(200).optional(),
body: z.string().min(1).optional(),
tags: z.array(z.string()).optional()
});
module.exports = { createPostSchema, updatePostSchema };
The Takeaway
Build a REST API with Express Router: define a schema, write a controller with asyncHandler and ApiError, write a router with auth and validation middlewares, mount the router under a path, and write Zod validation schemas. Each file has one job. The router stays thin and declarative.
Define a Mongoose schema, write a controller with asyncHandler and ApiError, write a router with auth and validation middlewares, mount the router in app.js, and write Zod validation schemas. Each file has one job; the router stays thin.
Parse the request, call a model or service, send the response with the right status code, and handle errors with ApiError. Check resource ownership for user-specific resources. Use asyncHandler so rejections go to the error handler.
Load the resource, compare its owner to req.user.userId. If they do not match and the user is not an admin, throw new ApiError(403, 'Not authorized'). Do this before updating or deleting.
Use a validate(schema) middleware factory that uses Zod's safeParse. Apply per route: router.post('/', validate(createPostSchema), createPost). Returns 400 with errors if invalid, replaces req.body with parsed data if valid.
In app.js: app.use('/api/v1/posts', require('./routes/posts')). The router's paths are relative to /api/v1/posts. Version your API from day one; bump on breaking changes.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

