Skip to main content

Routing in Express

Router

Use Router to group related routes into their own file:

src/routes/users.routes.ts
import { Router } from 'express';
import {
getUsers,
getUserById,
createUser,
updateUser,
deleteUser,
} from '../controllers/users.controller.js';
import { authenticate } from '../middleware/auth.middleware.js';

export const usersRouter = Router();

usersRouter.get('/', getUsers);
usersRouter.get('/:id', getUserById);
usersRouter.post('/', createUser);
usersRouter.put('/:id', authenticate, updateUser);
usersRouter.delete('/:id', authenticate, deleteUser);
src/routes/index.ts
import { Router } from 'express';
import { usersRouter } from './users.routes.js';
import { postsRouter } from './posts.routes.js';

export const apiRouter = Router();

apiRouter.use('/users', usersRouter);
apiRouter.use('/posts', postsRouter);
src/app.ts
app.use('/api', apiRouter);
// Results in: /api/users, /api/posts

Controllers

Controllers handle the request/response logic. Keep them thin — business logic belongs in a service layer:

src/controllers/users.controller.ts
import type { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/users.service.js';
import { sendSuccess } from '../utils/responses.js';

const userService = new UserService();

export async function getUsers(req: Request, res: Response, next: NextFunction) {
try {
const { page = '1', limit = '20', search } = req.query;

const result = await userService.getUsers({
page: parseInt(page as string, 10),
limit: parseInt(limit as string, 10),
search: search as string | undefined,
});

sendSuccess(res, result);
} catch (err) {
next(err); // pass to error handler
}
}

export async function getUserById(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const user = await userService.getById(id);

if (!user) {
res.status(404).json({ success: false, error: { message: 'User not found' } });
return;
}

sendSuccess(res, user);
} catch (err) {
next(err);
}
}

export async function createUser(req: Request, res: Response, next: NextFunction) {
try {
const user = await userService.create(req.body);
sendSuccess(res, user, 201);
} catch (err) {
next(err);
}
}

Route Parameters

// Path parameter
app.get('/users/:id', (req, res) => {
const { id } = req.params; // string
});

// Multiple parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
});

// Optional parameter
app.get('/users/:id?', (req, res) => {
const id = req.params.id; // string | undefined
});

Query Parameters

app.get('/products', (req, res) => {
const {
page = '1',
limit = '20',
sort = 'createdAt',
order = 'desc',
category,
q,
} = req.query;

// req.query values are always strings or string[]
// Parse them as needed
const pageNum = parseInt(page as string, 10);
const limitNum = Math.min(parseInt(limit as string, 10), 100); // cap at 100
});

Request Body

app.post('/users', (req, res) => {
const body = req.body; // parsed by express.json() middleware
// { name: 'Alice', email: 'alice@example.com' }
});

Typed Request Bodies

import type { Request, Response } from 'express';

interface CreateUserBody {
name: string;
email: string;
password: string;
}

export async function createUser(
req: Request<{}, {}, CreateUserBody>,
res: Response
) {
const { name, email, password } = req.body;
// TypeScript knows the types of name, email, password
}