CPCODELAB
Practice

Practice Express

You’ve read the lessons — now prove it. Take the quiz for instant feedback, then work the coding exercises. Try each one yourself before you reveal the solution.

Quick quiz

16 questions
Score: 0 / 16
0/16 answered
  1. 1

    Which method is used to start an Express server listening on a port?

  2. 2

    What does the following route match? app.get("/users/:id", handler)

  3. 3

    How do you access the route parameter id inside a handler for app.get("/users/:id", handler)?

  4. 4

    Which built-in Express middleware must you add to parse incoming JSON request bodies?

  5. 5

    What HTTP status code should a successful POST that creates a new resource return?

  6. 6

    In Express middleware, what must you call to pass control to the next middleware or route handler?

  7. 7

    Which of the following correctly sends a JSON response with status 404?

  8. 8

    What is the purpose of express.Router()?

  9. 9

    How does Express identify error-handling middleware?

  10. 10

    Which property of req contains URL query-string values for the route /search?term=express&page=2?

  11. 11

    What does the asyncWrapper (or catchAsync) pattern do in Express 4?

  12. 12

    Which express-rate-limit option controls the length of the counting window?

  13. 13

    When using Multer, what does upload.single("avatar") mean?

  14. 14

    Which security header does Helmet set to defend against clickjacking attacks?

  15. 15

    What is the key trade-off between server-side sessions and JWTs for authentication?

  16. 16

    Where should helmet() be placed in the middleware chain?

🛠️

Coding exercises

8 tasks
1

Hello Express Server

Easy

Create an Express server that listens on port 3000. Add a single GET / route that responds with the JSON { message: "Hello, Express!" } and status 200. Export the app for testing.

Starter
js
const express = require("express");
const app = express();

// TODO: add GET / route

app.listen(3000, () => console.log("Running on 3000"));
module.exports = app;
💡 Show hint

Use res.status(200).json(...) inside your route handler.

✅ Show solution
js
const express = require("express");
const app = express();

app.get("/", (req, res) => {
  res.status(200).json({ message: "Hello, Express!" });
});

app.listen(3000, () => console.log("Running on 3000"));
module.exports = app;
2

Route Params & Query Strings

Easy

Add two routes to an Express app: 1. GET /users/:id — respond with { userId: <id> } (the value from the URL). 2. GET /search — respond with { term: <term>, page: <page> } where term and page come from query-string parameters (?term=x&page=2). Default page to "1" if not provided.

Starter
js
const express = require("express");
const app = express();

// TODO: GET /users/:id

// TODO: GET /search

module.exports = app;
💡 Show hint

Access req.params.id for route params and req.query.term / req.query.page for query strings.

✅ Show solution
js
const express = require("express");
const app = express();

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

app.get("/search", (req, res) => {
  const term = req.query.term || "";
  const page = req.query.page || "1";
  res.json({ term, page });
});

module.exports = app;
3

Logger Middleware

Medium

Write a custom middleware function called requestLogger that logs "[METHOD] PATH" to the console for every incoming request (e.g. "[GET] /users/5"), then passes control to the next handler. Register it globally with app.use() before any routes. Also add a GET /ping route that responds with { status: "ok" }.

Starter
js
const express = require("express");
const app = express();

// TODO: write requestLogger middleware

// TODO: register middleware globally

// TODO: GET /ping route

module.exports = app;
💡 Show hint

Middleware receives (req, res, next). Call next() at the end so the request continues.

✅ Show solution
js
const express = require("express");
const app = express();

function requestLogger(req, res, next) {
  console.log(`[${req.method}] ${req.path}`);
  next();
}

app.use(requestLogger);

app.get("/ping", (req, res) => {
  res.json({ status: "ok" });
});

module.exports = app;
4

In-Memory CRUD REST API

Medium

Build a simple in-memory REST API for books. Each book has { id, title, author }. Implement: - GET /books — return all books. - GET /books/:id — return one book or 404 { error: "Not found" }. - POST /books — accept { title, author } from the JSON body, auto-assign a numeric id, store it, and return the new book with 201. - PUT /books/:id — update title and/or author; return updated book or 404. - DELETE /books/:id — remove the book; return 204 with no body, or 404. Use express.json() middleware.

Starter
js
const express = require("express");
const app = express();
app.use(express.json());

let books = [];
let nextId = 1;

// TODO: implement routes

module.exports = app;
💡 Show hint

Use Array.prototype.findIndex to locate a book by id. For DELETE with no body use res.status(204).send().

✅ Show solution
js
const express = require("express");
const app = express();
app.use(express.json());

let books = [];
let nextId = 1;

app.get("/books", (req, res) => {
  res.json(books);
});

app.get("/books/:id", (req, res) => {
  const book = books.find((b) => b.id === Number(req.params.id));
  if (!book) return res.status(404).json({ error: "Not found" });
  res.json(book);
});

app.post("/books", (req, res) => {
  const { title, author } = req.body;
  const book = { id: nextId++, title, author };
  books.push(book);
  res.status(201).json(book);
});

app.put("/books/:id", (req, res) => {
  const index = books.findIndex((b) => b.id === Number(req.params.id));
  if (index === -1) return res.status(404).json({ error: "Not found" });
  books[index] = { ...books[index], ...req.body };
  res.json(books[index]);
});

app.delete("/books/:id", (req, res) => {
  const index = books.findIndex((b) => b.id === Number(req.params.id));
  if (index === -1) return res.status(404).json({ error: "Not found" });
  books.splice(index, 1);
  res.status(204).send();
});

module.exports = app;
5

Modular Router & JWT Auth Middleware

Hard

Split your app into two files. `routes/products.js` — create an express.Router() that exposes: - GET / — list all products (use a local in-memory array). - POST / — add a product { id, name, price } from the JSON body (return 201). Export the router. `app.js` — mount the products router at /products. Add a JWT auth middleware that runs before the products router: - Read the Authorization header (Bearer <token>). - Verify the token using jsonwebtoken (jwt.verify(token, "secret")). - If valid, attach req.user = decoded and call next(). - If missing or invalid, respond 401 { error: "Unauthorized" }. Also add a global error-handling middleware that catches any thrown errors and returns 500 { error: err.message }. Export the app.

Starter
js
// routes/products.js
const express = require("express");
const router = express.Router();

let products = [];
let nextId = 1;

// TODO: GET / and POST /

module.exports = router;

// ---

// app.js
const express = require("express");
const jwt = require("jsonwebtoken");
const productsRouter = require("./routes/products");

const app = express();
app.use(express.json());

// TODO: JWT auth middleware

// TODO: mount productsRouter at /products

// TODO: error-handling middleware

module.exports = app;
💡 Show hint

Error-handling middleware must have the signature (err, req, res, next). Extract the Bearer token with header.split(" ")[1]. Wrap jwt.verify in a try/catch to handle invalid tokens.

✅ Show solution
js
// routes/products.js
const express = require("express");
const router = express.Router();

let products = [];
let nextId = 1;

router.get("/", (req, res) => {
  res.json(products);
});

router.post("/", (req, res) => {
  const { name, price } = req.body;
  const product = { id: nextId++, name, price };
  products.push(product);
  res.status(201).json(product);
});

module.exports = router;

// ---

// app.js
const express = require("express");
const jwt = require("jsonwebtoken");
const productsRouter = require("./routes/products");

const app = express();
app.use(express.json());

function authenticate(req, res, next) {
  const header = req.headers["authorization"];
  if (!header) return res.status(401).json({ error: "Unauthorized" });
  const token = header.split(" ")[1];
  try {
    req.user = jwt.verify(token, "secret");
    next();
  } catch {
    res.status(401).json({ error: "Unauthorized" });
  }
}

app.use("/products", authenticate, productsRouter);

// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

module.exports = app;
6

Rate Limiter Middleware

Medium

Using express-rate-limit, add two limiters to an Express app: 1. A global limiter that allows at most 100 requests per 15 minutes per IP. Apply it with app.use(). 2. A strict auth limiter that allows at most 5 requests per 15 minutes per IP. Apply it only to the /auth path prefix. Add a GET /ping route (returns { status: "ok" }) and a POST /auth/login route (returns { token: "fake-jwt" }) so the limiters can be exercised. Export the app.

Starter
js
const express = require("express");
const rateLimit = require("express-rate-limit");

const app = express();
app.use(express.json());

// TODO: global limiter (100 req / 15 min)

// TODO: auth limiter (5 req / 15 min) on /auth

// TODO: GET /ping

// TODO: POST /auth/login

module.exports = app;
💡 Show hint

Pass windowMs: 15 * 60 * 1000 and max: 100 (or 5) to rateLimit(). Mount the auth limiter with app.use("/auth", authLimiter) before your auth routes.

✅ Show solution
js
const express = require("express");
const rateLimit = require("express-rate-limit");

const app = express();
app.use(express.json());

const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false
});
app.use(globalLimiter);

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  standardHeaders: true,
  legacyHeaders: false
});
app.use("/auth", authLimiter);

app.get("/ping", (req, res) => {
  res.json({ status: "ok" });
});

app.post("/auth/login", (req, res) => {
  res.json({ token: "fake-jwt" });
});

module.exports = app;
7

File Upload Endpoint with Multer

Medium

Build a POST /upload/avatar route that accepts a single image file under the field name avatar using Multer. - Store uploaded files on disk in an uploads/ directory. - Generate a unique filename using Date.now() + the original file extension. - Restrict uploads to JPEG, PNG, and WebP only (reject others with a 400 error). - Cap file size at 2 MB. - On success respond with { filename, size } (values from req.file). Export the app.

Starter
js
const express = require("express");
const multer = require("multer");
const path = require("path");

const app = express();

// TODO: configure multer storage, fileFilter, and limits

// TODO: POST /upload/avatar route

module.exports = app;
💡 Show hint

Use multer.diskStorage() for the storage option. Inside fileFilter, call cb(null, true) to accept or cb(new Error("...")) to reject. Access the saved file via req.file.

✅ Show solution
js
const express = require("express");
const multer = require("multer");
const path = require("path");

const app = express();

const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, "uploads/"),
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname);
    cb(null, Date.now() + ext);
  }
});

const upload = multer({
  storage,
  limits: { fileSize: 2 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    const allowed = ["image/jpeg", "image/png", "image/webp"];
    if (allowed.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error("Only JPEG, PNG, and WebP are allowed"));
    }
  }
});

app.post("/upload/avatar", upload.single("avatar"), (req, res) => {
  if (!req.file) return res.status(400).json({ error: "No file uploaded" });
  res.json({ filename: req.file.filename, size: req.file.size });
});

// Multer error handler
app.use((err, req, res, next) => {
  res.status(400).json({ error: err.message });
});

module.exports = app;
8

Secure API with Helmet and CORS

Hard

Create an Express app that is secured with both Helmet and CORS: - Apply helmet() as the first middleware. - Configure cors() to allow requests only from https://example.com and https://staging.example.com. Enable the Authorization request header and allow credentials. - Reject all other origins with a CORS error. - Add a GET /api/status route that returns { ok: true }. - Add a centralized error-handling middleware (four arguments) that returns { error: err.message } with status 500 (or 403 if the error message contains the word CORS). Export the app.

Starter
js
const express = require("express");
const helmet = require("helmet");
const cors = require("cors");

const app = express();

// TODO: helmet first

// TODO: cors with allowlist

// TODO: GET /api/status

// TODO: error handler

module.exports = app;
💡 Show hint

Pass a function to the origin option of cors() — it receives (origin, callback). Call callback(null, true) to allow or callback(new Error("Not allowed by CORS")) to block. Remember !origin (server-to-server) is usually allowed.

✅ Show solution
js
const express = require("express");
const helmet = require("helmet");
const cors = require("cors");

const app = express();

app.use(helmet());

const allowedOrigins = ["https://example.com", "https://staging.example.com"];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error("Not allowed by CORS"));
    }
  },
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true
}));

app.get("/api/status", (req, res) => {
  res.json({ ok: true });
});

app.use((err, req, res, next) => {
  const isCors = err.message.includes("CORS");
  res.status(isCors ? 403 : 500).json({ error: err.message });
});

module.exports = app;