Request Validation & Security
Never Trust req.body
LinkedIn Hook
"Your Express server has a SQL injection vulnerability and you don't even know it."
Most Node.js tutorials teach you how to read
req.body.emailand shove it straight into a database query. They never mention that the value could be'; DROP TABLE users; --, or{ "$ne": null }, or a 50 MB JSON blob designed to crash your event loop.The single most important mindset shift in backend development is this: req.body is hostile until proven otherwise. Every property could be missing. Every string could be malicious. Every number could be a string. Every array could contain objects you didn't expect.
The fix is not "be careful." The fix is a validation layer at every boundary — Zod schemas that parse incoming data into typed, trusted shapes before any business logic touches it. Combine that with parameterized queries, helmet headers, CORS rules, and rate limiting, and your API stops being a buffet for attackers.
In Lesson 6.5, I break down the seven security layers every Express API needs — with Zod middleware patterns, real injection examples, and the "never trust the client" mental model interviewers love to test.
Read the full lesson -> [link]
#NodeJS #ExpressJS #WebSecurity #BackendDevelopment #InterviewPrep
What You'll Learn
- Why every request boundary needs a validation layer (params, query, body, headers)
- How to write reusable Zod schema middleware for Express routes
- The difference between validation (rejecting bad input) and sanitization (cleaning input)
- How parameterized queries kill SQL injection at the driver level
- Why output escaping — not input filtering — is the right XSS defense
- How to wire up
helmet,cors, andexpress-rate-limitcorrectly - The "never trust req.body" mindset and where developers most often break it
- When to use
express-validatorvs Zod, and why Zod usually wins in 2025
The Bouncer Analogy — Why Validation Belongs at the Door
Imagine a nightclub with a strict dress code, a 21+ age limit, and a maximum capacity. The owner has two choices.
Option A: Let everyone in, then walk around the dance floor occasionally checking IDs and outfits. By the time you find a problem, the underage drinker has already had three shots, the guy in flip-flops has already started a fight, and the fire marshal has already shut you down.
Option B: Put a bouncer at the door. Every single person who wants in must show ID, pass the dress check, and be counted against capacity. Anyone who fails is rejected on the spot. Nothing untrusted ever crosses the threshold.
Express APIs work the same way. Your route handlers, your database queries, your business logic — they are the dance floor. Every line of code inside them assumes the data is safe. The moment one untrusted value sneaks past the door, every assumption inside the building is invalid. SQL queries become injection vectors. JSON renders become XSS attacks. Numeric loops become denial-of-service.
The bouncer is your validation middleware. It runs before any handler logic, parses the request against a strict schema, and either hands a trusted, typed object to the handler — or rejects the request with a 400 before anyone downstream ever sees it.
+---------------------------------------------------------------+
| THE TRUST BOUNDARY |
+---------------------------------------------------------------+
| |
| UNTRUSTED ZONE TRUSTED ZONE |
| (the internet) (your handlers) |
| |
| +-----------+ +----------+ +----------+ +--------+ |
| | req.body |--->| validate |--->| handler |-->| DB | |
| | req.query | | (Zod) | | logic | | | |
| | req.params| +----------+ +----------+ +--------+ |
| +-----------+ | |
| | (rejected) |
| v |
| +---------+ |
| | 400 | |
| | Error | |
| +---------+ |
| |
| Rule: NOTHING crosses the boundary without parsing. |
| |
+---------------------------------------------------------------+
Napkin AI Visual Prompt: "Dark gradient (#0a1a0a -> #0d2e16). A vertical dashed white line down the center labeled 'Trust Boundary'. Left side labeled 'Untrusted (internet)' in red (#ef4444), showing chaotic envelopes labeled req.body, req.query, req.params. Center: a green (#68a063) gate labeled 'Zod validate'. Right side labeled 'Trusted (handlers)' in green, showing clean typed objects flowing into a database icon. Amber (#ffb020) arrows show the flow. White monospace labels."
Validation vs Sanitization — They Are Not The Same Thing
Beginners conflate these two words. They are different jobs with different goals.
- Validation asks: "Does this input match the shape and rules I expect?" If not, reject the request. Validation is binary — pass or fail. A field must be a string of 1-50 characters, a valid email, a positive integer. Anything else is an error.
- Sanitization asks: "How do I make this input safe to use in a specific context?" Sanitization transforms the input — escaping HTML for browser rendering, stripping dangerous SQL characters (almost never the right answer), trimming whitespace, normalizing email casing.
The two work together but at different times. Validate first to reject obviously bad input. Sanitize second, and only at the point of use, because what's safe in one context is dangerous in another. A <script> tag is fine in a Markdown post stored in your DB — it becomes dangerous only when rendered as HTML in a browser, which is where you escape it.
The most common beginner mistake is trying to sanitize at the input layer (stripping < characters from every form field "to prevent XSS") and then trusting the result everywhere. That approach is both insufficient (attackers find encodings you didn't strip) and corrupting (a user named O'Brien can no longer type their own name).
Example 1 — Reusable Zod Validation Middleware
Zod is the de-facto validation library for TypeScript Node in 2025. It defines a schema once and gives you both runtime parsing and a static type. We'll build a generic middleware that validates body, query, and params against schemas you supply per route.
// src/middleware/validate.ts
// A reusable Express middleware that parses req.body, req.query, and req.params
// against optional Zod schemas. Anything that fails parsing -> 400 with details.
import { Request, Response, NextFunction } from 'express';
import { ZodSchema, ZodError } from 'zod';
// The shape of schemas a route can supply. All three are optional.
interface ValidationSchemas {
body?: ZodSchema;
query?: ZodSchema;
params?: ZodSchema;
}
// Factory: returns an Express middleware bound to the supplied schemas.
export function validate(schemas: ValidationSchemas) {
return (req: Request, res: Response, next: NextFunction) => {
try {
// Parse each section if a schema was provided.
// .parse() throws ZodError on failure and returns a typed object on success.
// We REPLACE req.body/query/params with the parsed result so handlers
// downstream see only the trusted, coerced shape (e.g. "5" -> 5).
if (schemas.body) req.body = schemas.body.parse(req.body);
if (schemas.query) req.query = schemas.query.parse(req.query);
if (schemas.params) req.params = schemas.params.parse(req.params);
next();
} catch (err) {
// Zod errors carry a structured issues array. We translate it into
// a JSON response that frontends can map to form fields.
if (err instanceof ZodError) {
return res.status(400).json({
error: 'ValidationError',
issues: err.issues.map((i) => ({
path: i.path.join('.'),
message: i.message,
code: i.code,
})),
});
}
// Unknown error -> hand off to the central error middleware.
next(err);
}
};
}
Now use it on a route. Notice how the handler never re-checks anything — by the time it runs, every value is guaranteed to exist and have the right type.
// src/routes/users.ts
import { Router } from 'express';
import { z } from 'zod';
import { validate } from '../middleware/validate';
const router = Router();
// Define the schema OUTSIDE the route so it can be reused (e.g. in tests).
const createUserSchema = z.object({
// Trim whitespace, then require 1-50 chars after trimming.
name: z.string().trim().min(1).max(50),
// Built-in email validator. Lowercases for consistency.
email: z.string().email().toLowerCase(),
// Strong password rule. Min 12 chars, must contain a number.
password: z.string().min(12).regex(/[0-9]/, 'must contain a number'),
// Optional age, must be a positive integer if present.
age: z.number().int().positive().optional(),
});
// URL params arrive as strings. Use coerce to turn them into numbers.
const userIdParams = z.object({
id: z.coerce.number().int().positive(),
});
router.post('/users', validate({ body: createUserSchema }), (req, res) => {
// req.body is now FULLY TYPED and trusted. No defensive checks needed.
const { name, email, password, age } = req.body;
// ...persist the user
res.status(201).json({ name, email, age });
});
router.get('/users/:id', validate({ params: userIdParams }), (req, res) => {
// req.params.id is a number here, not a string, thanks to z.coerce.
res.json({ id: req.params.id });
});
export default router;
Why Zod over express-validator?
express-validatorworks by chainingbody('email').isEmail()calls, which mutates a context object you read withvalidationResult(req). It's fine, but you don't get TypeScript types out of it — you still have to write interfaces by hand. Zod gives youz.infer<typeof schema>for free, which is the single biggest reason it has taken over.
Example 2 — SQL Injection: Parameterized Queries vs String Concatenation
SQL injection is the oldest entry on the OWASP Top 10 and it still ships in production code every week. The vulnerability exists when user input is concatenated directly into a SQL string. The fix is parameterized queries (also called prepared statements), where the driver sends the SQL and the values to the database separately — the database never confuses one for the other.
// src/db/users.ts
import { pool } from './pool'; // pg connection pool
// =======================================================================
// VULNERABLE - DO NOT DO THIS
// =======================================================================
// Concatenating user input into the SQL string. Looks innocent.
// The attacker sends: email = "x' OR '1'='1"
// The query becomes: SELECT * FROM users WHERE email = 'x' OR '1'='1'
// -> returns EVERY user in the table.
// Worse payload: "x'; DROP TABLE users; --"
export async function findUserByEmail_BAD(email: string) {
const sql = `SELECT id, name, email FROM users WHERE email = '${email}'`;
const { rows } = await pool.query(sql);
return rows[0];
}
// =======================================================================
// SAFE - parameterized query
// =======================================================================
// $1 is a placeholder. The driver sends the SQL and the values array
// in two separate protocol messages. The database parses the SQL FIRST,
// then binds the values as data. Quotes inside the value can never
// "escape" into SQL syntax because the parser already finished.
export async function findUserByEmail(email: string) {
const sql = 'SELECT id, name, email FROM users WHERE email = $1';
const { rows } = await pool.query(sql, [email]);
return rows[0];
}
// Same idea with multiple parameters. Order matches $1, $2, $3...
export async function createUser(name: string, email: string, hash: string) {
const sql = `
INSERT INTO users (name, email, password_hash)
VALUES ($1, $2, $3)
RETURNING id, name, email
`;
const { rows } = await pool.query(sql, [name, email, hash]);
return rows[0];
}
A few rules that matter for interviews:
- There is no such thing as "escaping" SQL safely by hand. Use parameters. Always.
- ORMs (Prisma, Drizzle, Knex) use parameters under the hood. As long as you don't drop into raw query strings with concatenation, you're safe.
- Identifiers (table/column names) cannot be parameterized in most drivers. If you must accept a column name from the user, validate it against an allow-list of known column names — never concatenate.
Example 3 — Helmet + CORS: The Two Lines That Block 80% of Attacks
helmet sets a battery of HTTP response headers that browsers use to enforce security policies. cors controls which origins can call your API from a browser. Both should be installed in every Express project.
// src/app.ts
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
const app = express();
// ---------------------------------------------------------------
// helmet() applies a curated set of security headers. Defaults:
// - Content-Security-Policy (blocks inline scripts)
// - Strict-Transport-Security (forces HTTPS for 180 days)
// - X-Content-Type-Options: nosniff
// - X-Frame-Options: SAMEORIGIN (stops clickjacking)
// - Referrer-Policy: no-referrer
// - Cross-Origin-Resource-Policy: same-origin
// One line, ten attack classes blocked at the browser level.
// ---------------------------------------------------------------
app.use(helmet());
// ---------------------------------------------------------------
// CORS controls which browser ORIGINS can read responses from this API.
// NEVER use `cors()` with no options in production - that allows ANY
// origin, which means any malicious site can read your authenticated
// responses if the browser sends cookies.
// ---------------------------------------------------------------
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
app.use(
cors({
// Function form lets us check each request and allow same-origin /
// server-to-server requests (which have no Origin header at all).
origin: (origin, callback) => {
if (!origin) return callback(null, true); // curl, server-to-server
if (allowedOrigins.includes(origin)) return callback(null, true);
return callback(new Error('Not allowed by CORS'));
},
credentials: true, // allow cookies / Authorization
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400, // cache preflight for 24 hours
})
);
// Body parsers come AFTER security middleware. Limit JSON body size to
// stop attackers from sending a 100 MB blob to OOM your server.
app.use(express.json({ limit: '100kb' }));
A few things to internalize:
- CORS is a browser-enforced policy, not a firewall. A Python script with
requestswill happily call your API regardless of CORS. CORS exists to stop browsers from leaking authenticated responses to evil sites. origin: '*'andcredentials: trueare mutually exclusive by spec. Browsers reject this combination.- Helmet is not a substitute for input validation. It hardens response headers; it doesn't look at request bodies.
Example 4 — Rate Limiting with express-rate-limit
Without rate limiting, an attacker can brute-force passwords, scrape your data, or simply DoS your endpoints with a for loop. express-rate-limit puts a sliding-window counter in front of any route.
// src/middleware/rateLimit.ts
import rateLimit from 'express-rate-limit';
// ---------------------------------------------------------------
// Generic limiter for the whole API. 100 requests per 15 minutes
// per IP. Returns RFC-compliant headers: RateLimit-Limit,
// RateLimit-Remaining, RateLimit-Reset.
// ---------------------------------------------------------------
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100, // 100 requests per window per IP
standardHeaders: 'draft-7', // modern RateLimit-* headers
legacyHeaders: false, // disable old X-RateLimit-* headers
message: { error: 'Too many requests, please try again later.' },
});
// ---------------------------------------------------------------
// Strict limiter for the login endpoint. 5 attempts per 15 minutes
// per IP. This is the layer that stops credential stuffing.
// ---------------------------------------------------------------
export const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 5,
// Skip counting successful logins so legitimate users aren't punished
// for typing their password right.
skipSuccessfulRequests: true,
message: { error: 'Too many login attempts. Try again in 15 minutes.' },
});
// src/app.ts
import { apiLimiter, loginLimiter } from './middleware/rateLimit';
// Apply the generic limiter to every /api route.
app.use('/api', apiLimiter);
// Apply the strict limiter ONLY to login. Order matters - more
// specific limiter goes BEFORE the route handler.
app.post('/api/auth/login', loginLimiter, loginHandler);
Production notes:
- Behind a reverse proxy (nginx, Cloudflare, AWS ALB) you must call
app.set('trust proxy', 1). Otherwise every request looks like it came from the proxy's IP and one user can exhaust the limit for everyone. - Memory store is fine for a single instance, but useless across multiple servers. For a real deployment use the Redis store (
rate-limit-redis) so all your Node processes share one counter.
Example 5 — XSS: Escape Output, Don't "Sanitize" Input
Cross-Site Scripting (XSS) happens when user input is rendered into an HTML page without escaping, allowing the input to become executable JavaScript in another user's browser. The correct defense is escape at the point of rendering, not "strip dangerous characters at the input layer."
// src/views/render.ts
// Server-side rendering case. We control the HTML output, so we escape
// every user-supplied value before it touches the response.
import escapeHtml from 'escape-html';
interface Comment {
author: string;
body: string;
}
// BAD - directly interpolating user data into HTML.
// If body = "<script>fetch('/steal?c='+document.cookie)</script>"
// every visitor who loads this page now runs the attacker's script.
export function renderCommentBAD(c: Comment): string {
return `<div class="comment">
<strong>${c.author}</strong>: ${c.body}
</div>`;
}
// GOOD - escapeHtml() converts <, >, &, ', " to their HTML entities.
// The browser renders the literal characters and never parses them as tags.
export function renderComment(c: Comment): string {
return `<div class="comment">
<strong>${escapeHtml(c.author)}</strong>: ${escapeHtml(c.body)}
</div>`;
}
// ---------------------------------------------------------------
// JSON API case. If your API responds with JSON and the FRONTEND
// renders it (React, Vue, Svelte), the framework escapes for you
// automatically when you do {comment.body}. The danger is when
// you use dangerouslySetInnerHTML / v-html / @html - then YOU
// have to sanitize with a library like DOMPurify on the client.
// ---------------------------------------------------------------
// If you actually need to allow SOME HTML (e.g. a rich-text comment),
// use a library like sanitize-html with an explicit allow-list.
import sanitizeHtml from 'sanitize-html';
export function cleanRichText(dirty: string): string {
return sanitizeHtml(dirty, {
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'li'],
allowedAttributes: { a: ['href', 'title'] },
// Only allow http(s) links - blocks javascript: URLs.
allowedSchemes: ['http', 'https', 'mailto'],
});
}
The mental model: store raw, escape on output. The same comment might be rendered as HTML on the website, as plain text in an email, and as JSON to a mobile app. Each context needs a different escape strategy. If you mangled the data on input, you've corrupted it for every other context permanently.
Common Mistakes
1. Trusting req.body because "the frontend already validated it."
The frontend is just a UI you happen to ship. Anyone can send a raw HTTP request with curl, Postman, or a script. Frontend validation is for UX (instant feedback). Backend validation is for security. You need both, but only one of them is load-bearing.
2. Validating in the route handler with if statements instead of a schema middleware.
Scattered if (!req.body.email) return res.status(400)... checks are forgotten, duplicated, and inconsistent. Centralize validation in a middleware that runs before any handler. One source of truth, one error format, one place to update when the contract changes.
3. Concatenating user input into SQL "just for this one query." There is no "just this once." Every concatenated query is a potential injection. Use parameters even when the input "looks safe" — today it's an integer ID, tomorrow someone refactors it to accept a search string.
4. Using cors() with no options.
app.use(cors()) reflects whatever Origin the request sends and sets Access-Control-Allow-Origin to it. Combined with credentials: true it's a wide-open door. Always pass an explicit origin allow-list.
5. Sanitizing input instead of escaping output for XSS.
Stripping < characters from form fields breaks user data (people legitimately type <3 or x < y) and doesn't actually stop XSS — there are dozens of encodings attackers use. Escape at render time with the right escaper for the right context.
6. Forgetting app.set('trust proxy', 1) behind a load balancer.
Without it, req.ip returns the proxy's IP, your rate limiter sees one client doing everything, and a single bad actor blocks every legitimate user. Set it as soon as you deploy.
7. Not limiting body size.
express.json() with no limit accepts arbitrarily large payloads. An attacker sends a 500 MB JSON blob, your event loop spends 10 seconds parsing it, and your server is dead. Set limit: '100kb' (or whatever your real maximum is).
Interview Questions
1. "Walk me through how you'd validate an incoming POST /users request in Express."
I'd define a Zod schema describing the expected body shape — name as a trimmed string of 1-50 chars, email as a lowercased valid email, password as a min-12-char string with complexity rules, and so on. Then I'd write a generic validate(schemas) middleware that takes an object of { body?, query?, params? } Zod schemas, calls schema.parse() on each section in a try/catch, replaces req.body with the parsed (typed and coerced) result, and on ZodError returns a 400 with a structured issues array the frontend can map to form fields. The route then mounts the middleware before the handler: router.post('/users', validate({ body: createUserSchema }), handler). Inside the handler, req.body is fully typed via z.infer<typeof schema> and I never re-check anything. The key principle is that validation happens at the boundary, exactly once, and the handler operates on trusted data only.
2. "What's the difference between SQL injection and XSS, and how do you defend against each?"
SQL injection is a server-side attack where untrusted input is concatenated into a SQL string and then executed by the database, letting the attacker change the meaning of the query — read other users' data, drop tables, escalate privileges. The fix is parameterized queries: the driver sends SQL and values in separate protocol messages, so values can never be interpreted as SQL syntax. XSS is a client-side attack where untrusted input is rendered into an HTML page without escaping, letting the attacker inject <script> tags that run in another user's browser and steal cookies or session tokens. The fix is escaping at render time — escape-html for server-rendered templates, automatic escaping in React/Vue/Svelte, and DOMPurify if you must render rich HTML. Both attacks share a root cause (mixing data with code) but they happen in different layers and need different defenses.
3. "Why is sanitizing input a worse defense against XSS than escaping output?"
Two reasons. First, sanitizing input is incomplete — there are countless encodings (HTML entities, URL encoding, Unicode homoglyphs, JS string escapes) that bypass naive filters, and attackers find new ones constantly. Second, sanitizing input corrupts data permanently. The same comment "I love <3 you" will be rendered in HTML on the website, plain text in an email, and JSON to a mobile app. Each context needs different escaping. If you stripped < at input time, the website is "safe" but you've destroyed valid data for every other consumer. The right model is: store the raw bytes the user sent, then escape contextually at the point of output. HTML rendering -> HTML-escape. SQL query -> parameterize. Shell command -> shell-escape. JSON -> the serializer handles it.
4. "How does CORS actually work, and why is cors() with no options dangerous?"
CORS is a browser-enforced policy. When a script on evil.com tries to fetch api.yourbank.com, the browser first checks whether the response includes an Access-Control-Allow-Origin header that names evil.com. If not, the browser refuses to give the response to the script — even though the network request succeeded. CORS exists specifically to stop browsers from leaking authenticated responses (cookies, auth headers) to attacker-controlled sites. The danger of app.use(cors()) with no options is that the default reflects whatever origin the request sends, effectively saying "any origin is allowed." Combined with credentials: true, that means a malicious page can make authenticated requests to your API on behalf of any logged-in user and read the responses. The fix is an explicit allow-list of trusted origins. Also worth noting: CORS only protects browsers. A script using curl or node-fetch ignores CORS entirely — it's not a firewall, it's a browser sandbox.
5. "Where would you place rate limiting in a production Express app, and what configuration matters?"
I'd apply a generic limiter (e.g. 100 req / 15 min per IP) to the whole /api namespace as a baseline, and a strict limiter (5 req / 15 min, with skipSuccessfulRequests: true) on auth endpoints like /auth/login, /auth/forgot-password, and /auth/verify-otp because those are the credential-stuffing targets. Three configuration details matter most. First, app.set('trust proxy', 1) if you're behind nginx/Cloudflare/ALB — otherwise every request looks like it came from the proxy and one user blocks everyone. Second, the store: the default in-memory store is per-process, so on a multi-instance deployment you need rate-limit-redis or all your replicas keep separate counters. Third, standardHeaders: 'draft-7' so clients get the modern RateLimit-* response headers and can back off intelligently. Rate limiting is your last line of defense, not your only one — pair it with strong password hashing and account lockout to actually stop brute force.
Quick Reference — Validation & Security Cheat Sheet
+---------------------------------------------------------------+
| NEVER TRUST req.body - 7 LAYERS |
+---------------------------------------------------------------+
| |
| 1. helmet() -> security response headers |
| 2. cors({ origin: [..] })-> browser origin allow-list |
| 3. express.json({limit}) -> body size cap |
| 4. rateLimit(...) -> request rate per IP |
| 5. validate({body,...}) -> Zod schema at the boundary |
| 6. parameterized queries -> $1, $2 - never concat SQL |
| 7. escapeHtml() on output-> XSS defense at render time |
| |
+---------------------------------------------------------------+
+---------------------------------------------------------------+
| ZOD QUICK PATTERNS |
+---------------------------------------------------------------+
| |
| z.string().trim().min(1).max(50) |
| z.string().email().toLowerCase() |
| z.string().min(12).regex(/[0-9]/) |
| z.number().int().positive() |
| z.coerce.number().int() // for URL params |
| z.enum(['admin','user']) |
| z.array(z.string()).max(10) |
| z.object({...}).strict() // reject unknown keys |
| type User = z.infer<typeof userSchema> |
| |
+---------------------------------------------------------------+
+---------------------------------------------------------------+
| THE GOLDEN RULES |
+---------------------------------------------------------------+
| |
| 1. Validate at the boundary, once, in middleware |
| 2. Replace req.body with the parsed result |
| 3. Parameterize EVERY SQL query - no exceptions |
| 4. Store raw, escape on output |
| 5. Allow-list origins in CORS, never reflect |
| 6. trust proxy = 1 behind any reverse proxy |
| 7. Limit body size and request rate |
| 8. Frontend validation is UX, backend validation is security |
| |
+---------------------------------------------------------------+
| Concern | Tool | One-line fix |
|---|---|---|
| Schema validation | zod | schema.parse(req.body) in middleware |
| Legacy validation | express-validator | body('email').isEmail() chains |
| Security headers | helmet | app.use(helmet()) |
| Cross-origin requests | cors | cors({ origin: allowList, credentials: true }) |
| Rate limiting | express-rate-limit | rateLimit({ windowMs, limit }) |
| SQL injection | pg / Prisma / Drizzle | parameterized queries ($1, ?) |
| HTML XSS | escape-html / DOMPurify | escape at render, never at input |
| Rich-text XSS | sanitize-html | explicit tag/attribute allow-list |
| Body size DoS | express.json | { limit: '100kb' } |
Prev: Lesson 6.4 -- Error Handling Middleware Next: Lesson 7.1 -- Connecting to Databases
This is Lesson 6.5 of the Node.js Interview Prep Course -- 10 chapters, 42 lessons.