Express.js REST API — Complete Tutorial

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Express.js remains the most popular Node.js framework. It's lightweight, flexible, and perfect for building REST APIs at any scale.

Setting Up Express

npm install express cors dotenv
npm install --save-dev typescript @types/express ts-node
import express, { Express } from "express";
import cors from "cors";
import dotenv from "dotenv";

dotenv.config();

const app: Express = express();
const port = process.env.PORT || 3000;

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors());

app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Routing

// Simple routes
app.get("/health", (req, res) => {
  res.json({ status: "OK" });
});

// Route parameters
app.get("/users/:id", (req, res) => {
  const { id } = req.params;
  res.json({ userId: id });
});

// Query parameters
app.get("/products", (req, res) => {
  const { limit = 10, offset = 0 } = req.query;
  res.json({ limit, offset });
});

// Request body
app.post("/users", (req, res) => {
  const { name, email } = req.body;
  res.status(201).json({ id: 1, name, email });
});

// Route handlers
const getUser = (req: any, res: any) => {
  res.json({ id: 1, name: "Alice" });
};

app.get("/api/users/:id", getUser);

// Method chaining
app.route("/users/:id")
  .get((req, res) => res.json({ id: req.params.id }))
  .put((req, res) => res.json({ updated: true }))
  .delete((req, res) => res.json({ deleted: true }));

Middleware

// Logger middleware
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next();
});

// Authentication middleware
interface AuthRequest extends express.Request {
  user?: { id: number; email: string };
}

const authMiddleware = (req: AuthRequest, res: any, next: any) => {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ error: "No token" });

  req.user = { id: 1, email: "user@example.com" };
  next();
};

app.use(authMiddleware);

// Error handling middleware (must be last)
app.use(
  (err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
    console.error(err);
    res.status(err.status || 500).json({ error: err.message });
  }
);

Controllers and Services

// User service
class UserService {
  async getUser(id: number): Promise<User> {
    return { id, name: "Alice", email: "alice@example.com" };
  }

  async createUser(name: string, email: string): Promise<User> {
    return { id: 1, name, email };
  }
}

// User controller
class UserController {
  private service = new UserService();

  async getUser(req: express.Request, res: express.Response) {
    const { id } = req.params;
    const user = await this.service.getUser(parseInt(id));
    res.json(user);
  }

  async createUser(req: express.Request, res: express.Response) {
    const { name, email } = req.body;
    const user = await this.service.createUser(name, email);
    res.status(201).json(user);
  }
}

// Use controller
const userController = new UserController();
app.get("/users/:id", (req, res) => userController.getUser(req, res));
app.post("/users", (req, res) => userController.createUser(req, res));

Error Handling

// Custom error class
class ApiError extends Error {
  constructor(
    public statusCode: number,
    message: string
  ) {
    super(message);
  }
}

// Async error wrapper
const asyncHandler = (fn: any) => (req: any, res: any, next: any) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Usage
app.get(
  "/users/:id",
  asyncHandler(async (req: any, res: any) => {
    const user = await getUser(req.params.id);
    if (!user) throw new ApiError(404, "User not found");
    res.json(user);
  })
);

// Global error handler
app.use((err: any, req: any, res: any, next: any) => {
  const status = err.statusCode || 500;
  const message = err.message || "Internal server error";
  res.status(status).json({ error: message });
});

Validation

import { body, param, validationResult } from "express-validator";

// Validate request
app.post(
  "/users",
  [
    body("name").notEmpty().isString(),
    body("email").isEmail(),
    body("age").optional().isInt({ min: 0, max: 120 }),
  ],
  (req: any, res: any) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    const { name, email, age } = req.body;
    res.json({ id: 1, name, email, age });
  }
);

Advanced Patterns

Router Organization

// routes/users.ts
import { Router } from "express";

const router = Router();

router.get("/", (req, res) => res.json([]));
router.get("/:id", (req, res) => res.json({ id: req.params.id }));
router.post("/", (req, res) => res.status(201).json({ id: 1 }));
router.put("/:id", (req, res) => res.json({ updated: true }));
router.delete("/:id", (req, res) => res.json({ deleted: true }));

export default router;

// app.ts
import userRoutes from "./routes/users";
app.use("/api/users", userRoutes);

Rate Limiting

import rateLimit from "express-rate-limit";

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
});

app.use("/api/", limiter);

// Stricter limits for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: "Too many login attempts",
});

app.post("/auth/login", authLimiter, (req, res) => {
  // Handle login
});

Compression

import compression from "compression";

// Compress all responses
app.use(compression());

Testing

import request from "supertest";

describe("User API", () => {
  it("should get user by ID", async () => {
    const res = await request(app).get("/users/1");
    expect(res.status).toBe(200);
    expect(res.body).toHaveProperty("id");
  });

  it("should create user", async () => {
    const res = await request(app)
      .post("/users")
      .send({ name: "Alice", email: "alice@example.com" });
    expect(res.status).toBe(201);
  });
});

FAQ

Q: Should I use Express in 2025? A: Yes, it's battle-tested. For new projects, also consider Fastify or Hono for better performance.

Q: How do I handle file uploads in Express? A: Use multer middleware. It handles multipart form data.

Q: What's the best way to structure a large Express application? A: Use feature-based structure with routes, controllers, and services separated by domain.


Express.js remains excellent for REST APIs. Its simplicity means you focus on your business logic, not framework magic. Master it and you can build APIs that scale.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro