Skip to content

Building REST APIs with Node.js

Node.js with Express is one of the most popular stacks for REST APIs. This page covers the patterns needed for production-grade APIs.

Terminal window
npm install express
npm install -D @types/express typescript ts-node
import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
const users: User[] = [];
app.get('/api/users', (req: Request, res: Response) => {
res.json(users);
});
app.get('/api/users/:id', (req: Request, res: Response) => {
const user = users.find(u => u.id === req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
app.post('/api/users', (req: Request, res: Response) => {
const user = { id: crypto.randomUUID(), ...req.body };
users.push(user);
res.status(201).json(user);
});
app.put('/api/users/:id', (req: Request, res: Response) => {
const index = users.findIndex(u => u.id === req.params.id);
if (index === -1) return res.status(404).json({ error: 'User not found' });
users[index] = { ...users[index], ...req.body };
res.json(users[index]);
});
app.delete('/api/users/:id', (req: Request, res: Response) => {
const index = users.findIndex(u => u.id === req.params.id);
if (index === -1) return res.status(404).json({ error: 'User not found' });
users.splice(index, 1);
res.status(204).send();
});
app.listen(3000, () => console.log('API running on port 3000'));

Split routes into separate files:

src/routes/users.ts
import { Router } from 'express';
const router = Router();
router.get('/', getUsers);
router.get('/:id', getUserById);
router.post('/', createUser);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);
export default router;
src/app.ts
import usersRouter from './routes/users';
app.use('/api/users', usersRouter);
Terminal window
npm install zod
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150),
});
function validateBody(schema: z.ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
req.body = result.data;
next();
};
}
router.post('/', validateBody(CreateUserSchema), createUser);
// Async wrapper to avoid try/catch in every route
function asyncHandler(fn: RequestHandler): RequestHandler {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
// Global error handler — must have 4 parameters
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: err.message });
});
Terminal window
npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
// Generate token
function generateToken(userId: string): string {
return jwt.sign({ sub: userId }, JWT_SECRET, { expiresIn: '7d' });
}
// Verify token middleware
function authenticate(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'No token' });
try {
const payload = jwt.verify(token, JWT_SECRET) as { sub: string };
req.userId = payload.sub;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
// Protected route
router.get('/profile', authenticate, (req, res) => {
res.json({ userId: req.userId });
});
// Consistent response shape
function sendSuccess(res: Response, data: unknown, status = 200) {
res.status(status).json({ success: true, data });
}
function sendError(res: Response, message: string, status = 400) {
res.status(status).json({ success: false, error: message });
}
// Pagination
app.get('/api/posts', (req, res) => {
const page = Number(req.query.page) || 1;
const limit = Math.min(Number(req.query.limit) || 20, 100);
const offset = (page - 1) * limit;
const items = posts.slice(offset, offset + limit);
res.json({
data: items,
pagination: { page, limit, total: posts.length },
});
});
StatusMeaning
200OK
201Created
204No Content (successful delete)
400Bad Request (validation error)
401Unauthorized (not authenticated)
403Forbidden (authenticated but no permission)
404Not Found
409Conflict (duplicate resource)
422Unprocessable Entity
500Internal Server Error