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- 1
Which method is used to start an Express server listening on a port?
- 2
What does the following route match?
app.get("/users/:id", handler) - 3
How do you access the route parameter
idinside a handler forapp.get("/users/:id", handler)? - 4
Which built-in Express middleware must you add to parse incoming JSON request bodies?
- 5
What HTTP status code should a successful POST that creates a new resource return?
- 6
In Express middleware, what must you call to pass control to the next middleware or route handler?
- 7
Which of the following correctly sends a JSON response with status 404?
- 8
What is the purpose of
express.Router()? - 9
How does Express identify error-handling middleware?
- 10
Which property of
reqcontains URL query-string values for the route/search?term=express&page=2? - 11
What does the
asyncWrapper(orcatchAsync) pattern do in Express 4? - 12
Which
express-rate-limitoption controls the length of the counting window? - 13
When using Multer, what does
upload.single("avatar")mean? - 14
Which security header does Helmet set to defend against clickjacking attacks?
- 15
What is the key trade-off between server-side sessions and JWTs for authentication?
- 16
Where should
helmet()be placed in the middleware chain?
Coding exercises
8 tasksHello Express Server
EasyCreate 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.
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
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;
Route Params & Query Strings
EasyAdd 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.
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
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;
Logger Middleware
MediumWrite 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" }.
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
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;
In-Memory CRUD REST API
MediumBuild 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.
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
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;
Modular Router & JWT Auth Middleware
HardSplit 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.
// 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
// 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;
Rate Limiter Middleware
MediumUsing 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.
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
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;
File Upload Endpoint with Multer
MediumBuild 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.
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
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;
Secure API with Helmet and CORS
HardCreate 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.
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
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;