Episode 3 — NodeJS MongoDB Backend Architecture / 3.4 — Express JS
3.4 — Express.js: Quick Revision
Episode 3 supplement -- print-friendly cheat sheet.
How to use
Skim -> drill weak spots in 3.4.a through 3.4.f -> 3.4-Exercise-Questions.md -> 3.4-Interview-Questions.md.
Express Setup (Minimal)
npm init -y
npm install express
// app.js
const express = require('express');
const app = express();
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse form bodies
app.get('/', (req, res) => res.send('Hello'));
module.exports = app;
// server.js
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Why separate? app.js exports the app for testing (Supertest). server.js starts the server.
Routing Syntax
// Basic routes
app.get('/api/users', handler); // Read all
app.get('/api/users/:id', handler); // Read one
app.post('/api/users', handler); // Create
app.put('/api/users/:id', handler); // Replace
app.patch('/api/users/:id', handler); // Partial update
app.delete('/api/users/:id', handler); // Delete
// Router (modular)
const router = express.Router();
router.get('/', getAllUsers);
router.get('/:id', getUser);
router.post('/', createUser);
module.exports = router;
// Mount in app.js
app.use('/api/users', require('./routes/userRoutes'));
Response Methods Table
| Method | Content-Type | Use Case |
|---|---|---|
res.send(string) | text/html | Send HTML or plain text |
res.send(object) | application/json | Auto-detects, but prefer res.json() |
res.json(object) | application/json | API responses (always use for APIs) |
res.status(201).json(data) | application/json | Set status code + send JSON |
res.sendStatus(204) | n/a | Send status code with default body |
res.redirect(301, '/new-url') | n/a | Redirect client |
res.sendFile(absolutePath) | auto-detected | Send a file |
res.download(filePath) | auto-detected | Send file as attachment (triggers download) |
res.set('X-Custom', 'value') | n/a | Set a response header |
res.cookie('name', 'val', opts) | n/a | Set a cookie |
HTTP Methods Table
| Method | CRUD | Idempotent | Has Body | Example |
|---|---|---|---|---|
GET | Read | Yes | No | GET /api/users/42 |
POST | Create | No | Yes | POST /api/users + JSON body |
PUT | Replace | Yes | Yes | PUT /api/users/42 + full object |
PATCH | Partial update | Yes | Yes | PATCH /api/users/42 + { name } |
DELETE | Delete | Yes | Optional | DELETE /api/users/42 |
Idempotent = same request N times = same result. POST is not idempotent (creates duplicates).
Query Params vs Route Params
Route Params (req.params) | Query Params (req.query) | |
|---|---|---|
| Defined in | Route path: /users/:id | Query string: ?key=value |
| Purpose | Identify a specific resource | Filter, sort, paginate results |
| Required? | Yes (unless /:id? makes it optional) | Always optional |
| Type | String (always) | String (always) |
| Example URL | /api/users/42 | /api/users?role=admin&page=2 |
| Access | req.params.id -> '42' | req.query.role -> 'admin' |
// Combined example
// Route: GET /api/teams/:teamId/members?role=admin&page=1
app.get('/api/teams/:teamId/members', (req, res) => {
const teamId = req.params.teamId; // '7'
const role = req.query.role; // 'admin'
const page = Number(req.query.page); // 1
});
Middleware Essentials
// Application-level (runs for ALL requests)
app.use(express.json());
app.use(morgan('dev'));
// Path-specific
app.use('/api', authMiddleware);
// Route-specific (inline)
app.get('/admin', requireAdmin, (req, res) => { ... });
// Error handler (MUST be last, MUST have 4 params)
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).json({ error: err.message });
});
Order matters: Middleware runs top to bottom. Body parser must come before routes that read req.body.
Static Files Configuration
// Basic: public/css/style.css -> /css/style.css
app.use(express.static(path.join(__dirname, 'public')));
// With prefix: public/css/style.css -> /assets/css/style.css
app.use('/assets', express.static(path.join(__dirname, 'public')));
// With options (production)
app.use(express.static(path.join(__dirname, 'public'), {
maxAge: '1d', // Browser cache duration
dotfiles: 'deny', // Block .env, .gitignore
index: false // Disable directory listing
}));
Always use path.join(__dirname, 'public') -- relative paths resolve from process.cwd(), not from the file location.
SPA Fallback Pattern
// 1. API routes first
app.use('/api', apiRouter);
// 2. Static files second
app.use(express.static(path.join(__dirname, 'public')));
// 3. SPA fallback last -- serve index.html for all unmatched GET requests
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
Order: API routes -> static files -> SPA fallback. The wildcard must be last.
Common Patterns
404 Catch-All
// After all routes, before error handler
app.use((req, res) => {
res.status(404).json({ status: 'fail', message: 'Route not found' });
});
Consistent API Response Format
// Success
{ status: 'success', data: { user: { ... } } }
// Error
{ status: 'fail', message: 'User not found' }
// Server error
{ status: 'error', message: 'Internal server error' }
Parse + Validate Params
app.get('/api/users/:id', (req, res, next) => {
const id = Number(req.params.id);
if (isNaN(id) || id < 1) {
return res.status(400).json({ error: 'Invalid ID' });
}
// proceed...
});
Express vs Raw http Module
| Feature | Raw http | Express |
|---|---|---|
| Routing | if/else on req.url | app.get(), app.post(), etc. |
| Body parsing | Read stream + JSON.parse() | express.json() |
| Query params | url.parse() or new URL() | req.query auto-parsed |
| Route params | Manual regex | /users/:id -> req.params |
| Response helpers | res.writeHead() + res.end() | res.json(), res.status() |
| Static files | Manual fs.readFile() | express.static() |
| Middleware | Manual function chaining | app.use() pipeline |
Common Errors
| Error | Cause | Fix |
|---|---|---|
req.body is undefined | Missing express.json() | Add app.use(express.json()) before routes |
Cannot set headers after they are sent | Double res.send() / missing return | Add return before res.json() in conditionals |
| Route not matching | Wrong method or path | Check app.get vs app.post, check path spelling |
| Static file 404 | Wrong path or missing middleware | Use path.join(__dirname, 'public') |
req.params.id is a string | Params are always strings | Cast with Number() or parseInt() |
| 404 handler catches everything | Placed before routes | Move after all route definitions |
One-Liners
- Express = thin wrapper over Node
httpmodule. Routing + middleware + helpers. app.use()= register middleware. Runs for every matching request, in order.express.json()= parse JSON bodies. Without it,req.bodyisundefined.req.params= route segments (:id).req.query= query string (?key=val). Both are strings.res.json()= always use for API responses. Explicit JSON Content-Type.express.static()= serve files from a directory. Use absolute paths.- Error handler = 4 params
(err, req, res, next). Must be registered last. - Separate app.js / server.js = testability. Import app without starting server.
End of 3.4 quick revision.