The server from the previous lesson has a flaw that is embarrassing to show: it answers exactly the same thing to GET /events, to POST /orders and to DELETE /wipe-everything, always with a 200 OK. It does not look at the method, it does not look at the path and it does not look at the parameters. It is a deaf server.
In this lesson we give it ears and a voice. We are going to take apart the two objects the handler receives —req and res—, learn how to extract information from the request properly, and build the vocabulary Escena Viva will answer with for the rest of the course: the status codes, the headers that matter and a helper module, src/server/responses.js, along with the piece that will travel the furthest: the table that maps our domain error.appCode values to HTTP statuses. That table will survive the arrival of Express in Module 6.
Contents
req: the anatomy ofIncomingMessagereq.urlis not a URL: parsing it withnew URLres: the anatomy ofServerResponseERR_HTTP_HEADERS_SENT: the module's most frequent error- The status codes the course will use
- The headers that matter
src/server/responses.js- From domain
error.appCodeto HTTP status - Redirects and the
HEADmethod
req: the anatomy of IncomingMessage
req: the anatomy of IncomingMessageThe handler's first argument is an instance of http.IncomingMessage. And the first thing to know about it is that it is a readable stream: IncomingMessage extends stream.Readable, with everything that implies since lesson 03-04 (data and end events, for await...of, backpressure). Node hands you the object as soon as it has finished reading the headers; the body may still be travelling over the network while your handler is already running. That is why the body has to be read patiently, and that is why it gets its own lesson (04-05).
The properties you use daily:
console.error(req.method); // 'GET' (ALWAYS uppercase)
console.error(req.url); // '/events?venue=Teatro%20Almendra'
console.error(req.httpVersion); // '1.1'
console.error(req.headers['user-agent']); // 'curl/8.5.0'
console.error(req.headers.host); // 'localhost:3000'
console.error(req.socket.remoteAddress); // '::ffff:127.0.0.1'| Property | Type | The surprising detail |
|---|---|---|
req.method |
String | Always uppercase; compare it as is, without toUpperCase() |
req.url |
String | Path and query only. It never carries the scheme or the domain |
req.headers |
Object | Keys always lowercase, however the client sent them |
req.headers['set-cookie'] |
Array | It is the only header Node always delivers as an array |
req.socket.remoteAddress |
String | The IP of the last hop, not necessarily the user's |
req.rawHeaders |
Array | Flat pairs with the client's original capitalization |
Two warnings worth money. The first: the keys of req.headers are normalized to lowercase, so req.headers['Content-Type'] is undefined and req.headers['content-type'] works. The bug is subtle because it raises no error, just a silent undefined. The second: behind a reverse proxy or a load balancer, req.socket.remoteAddress will give you the proxy's IP; the real client's arrives in X-Forwarded-For, a header that is only trustworthy if you control the proxy writing it. We will come back to it when rate-limiting requests by IP in lesson 08-06.
req.url is not a URL: parsing it with new URL
req.url is not a URL: parsing it with new URLThis is where nearly everyone writes their first bug. req.url is the protocol's request URL: a path with its query string, with no origin. And the temptation is to treat it as text:
// WRONG: don't do this
const [path, query] = req.url.split('?');
const venue = query.split('=')[1];That code fails in four real scenarios:
- It does not decode. With
GET /events?venue=Sala%20B%C3%B3vedayou get the literal stringSala%20B%C3%B3veda, which matches neither'Sala Boveda'nor anything else. - It breaks with more than one parameter.
?venue=X&sort=dateleavesvenueholdingX&sort=date. - It ignores repeated parameters.
?category=jazz&category=comedyis valid HTTP and means two values. - It does not account for the fragment or redundant slashes, nor does it normalize anything.
The right tool is the URL class, global since Node 10 and a browser standard. It needs an absolute URL, so you pass it a base:
// The base is only there to complete the origin: what matters is the pathname.
const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
url.pathname; // '/events' (already mostly decoded)
url.searchParams.get('venue'); // 'Sala Boveda' <- decoded
url.searchParams.get('nonexistent'); // null
url.searchParams.getAll('category'); // ['jazz', 'comedy']
url.searchParams.has('detail'); // true even if it comes empty: '?detail='searchParams is a URLSearchParams: it decodes percent-encoding and +, supports repeated keys with getAll, and it is iterable. A practical check against our catalog:
One important detail about pathname: the URL class does not fully decode the path (for example %2F stays %2F, and rightly so: an encoded slash must not turn into a segment separator). If an identifier can carry encoded characters, apply decodeURIComponent to each segment separately, never to the whole path. It is a precaution we will pick up again when extracting route parameters in the next lesson.
A useful pattern for reading numeric parameters with a default value and validation:
function readPositiveInteger(searchParams, name, fallback) {
const raw = searchParams.get(name);
if (raw === null) return fallback;
const value = Number(raw);
if (!Number.isInteger(value) || value < 1) {
const error = new Error(`The "${name}" parameter must be a positive integer`);
error.appCode = 'INVALID_QUANTITY'; // Domain vocabulary: this will be a 400
throw error;
}
return value;
}
res: the anatomy of ServerResponse
res: the anatomy of ServerResponseThe second argument is an http.ServerResponse, and its nature is symmetrical to req's: it is a writable stream, ServerResponse extends stream.Writable. That means res.write() returns false when the buffer fills up, that it emits drain, and that you can hook a pipeline to it —exactly what we will do in lesson 04-04 to serve files.
A response is built in three steps, and the order is not negotiable:
res.statusCode = 200; // 1. Status
res.setHeader('Content-Type', 'application/json; charset=utf-8'); // and headers
res.write('{"events":'); // 2. Body,
res.write('3}'); // in 1 or N writes
res.end(); // 3. End (MANDATORY)The methods, each with its nuance:
| Method | What it does | When to use it |
|---|---|---|
res.statusCode = 200 |
Sets the status | Always; explicit even for 200 |
res.setHeader(n, v) |
Sets a header; it can be overwritten and read back | The usual choice |
res.writeHead(200, obj) |
Status + headers and sends them right away | A shortcut, for a short response |
res.write(data) |
Writes a chunk; sends the headers the first time | Long or streaming responses |
res.end([data]) |
Writes the last chunk and closes | Always, on every branch |
res.headersSent |
true if the headers already went out |
Before trying to change them |
writeHead and setHeader can be combined —writeHead wins in case of conflict—, but mixing styles is confusing. In Escena Viva we will use setHeader for anything cumulative and writeHead only when the response closes immediately.
ERR_HTTP_HEADERS_SENT: the module's most frequent error
ERR_HTTP_HEADERS_SENT: the module's most frequent errorHTTP sends the headers before the body, because that is how the message travels. And once a byte has gone out through the socket, there is no way to get it back. That is why this code blows up:
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end('All good');
res.setHeader('X-Too-Late', 'yes');
// Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the clientIn practice, the error is almost never that obvious: it shows up when one function responds and execution continues into another that responds as well.
async function handle(req, res) {
if (!req.url.startsWith('/events')) {
sendError(res, 404, 'Route not found');
// A return IS MISSING: execution continues and below we respond again
}
const events = await getCatalog();
sendJson(res, 200, events); // <- blows up if we already responded
}The three rules that prevent it forever:
- Responding means finishing. Every call to a response helper is preceded by
return(return sendError(...)), or lives inside anif/elsewith no way out. - A handler responds exactly once. If you need to choose between several branches, compute first and respond at the end.
- In
catchblocks, checkres.headersSentbefore trying to respond with a 500: if the failure happened halfway through a stream, all you can do isres.end()orres.destroy(), because the client already received a200that you will not be able to take back.
- The status codes the course will use
The first digit gives the family: 2xx went well, 3xx look elsewhere, 4xx you got it wrong (the client), 5xx I got it wrong (the server). That border between 4xx and 5xx is the most important of all: a 500 is an alert for the developer; a 4xx is not.
| Code | Name | Exact meaning in Escena Viva |
|---|---|---|
| 200 | OK | GET /events with the catalog, GET /events/evt-001 with the event |
| 201 | Created | A POST /orders has created the order; it comes with Location |
| 204 | No Content | DELETE /orders/ord-7 succeeded. No body, and no Content-Type |
| 304 | Not Modified | The browser already has styles.css cached and it is still valid |
| 400 | Bad Request | Malformed JSON or a quantity that is not a positive integer |
| 401 | Unauthorized | The token is missing or invalid: I don't know who you are (Module 8) |
| 403 | Forbidden | I know who you are, but org-boveda cannot edit an event of org-ribera |
| 404 | Not Found | evt-999 does not exist, or the route is not registered |
| 405 | Method Not Allowed | DELETE /events: the route exists, the method does not. Requires an Allow header |
| 409 | Conflict | Insufficient capacity: the request is valid, the current state prevents it |
| 422 | Unprocessable Content | Correct syntax and impossible semantics: 8 tickets with a maximum of 6 |
| 429 | Too Many Requests | Too many requests from one IP (rate limiting, 08-06) |
| 500 | Internal Server Error | An exception we could not classify. It is our fault |
| 503 | Service Unavailable | The currency API is not responding and we cannot degrade (04-06) |
Three distinctions that come up in any interview and, more importantly, that any badly built API gets wrong:
- 400 versus 422.
400is "I don't understand what you're sending me" (broken JSON, wrong type).422is "I understand you perfectly and your request makes no sense" (you ask for 8 tickets with a limit of 6). Many APIs use400for both; we will distinguish, because the resulting error message is far more useful. - 401 versus 403.
401is "I don't know who you are" and usually comes withWWW-Authenticate;403is "I know who you are and I won't let you". The official names are the reverse of what they seem, which is why they get mixed up. - 404 versus 409. If the session does not exist,
404. If it exists but there are no tickets left,409: the resource is there, what fails is the state.
- The headers that matter
Content-Type declares what you are sending. For text, always with charset:
Without charset=utf-8, an old browser can render Bóveda as Bóveda. In JSON the standard already mandates UTF-8, but declaring it costs nothing and in text/html and text/plain it is essential.
Content-Length versus Transfer-Encoding: chunked. If you know the exact size in bytes, declare it: the client can show a progress bar and reuse the connection better. If you do not know it —because you are generating the response on the fly or sending a stream—, Node uses chunked automatically, splitting the body into length-prefixed chunks.
const body = JSON.stringify(data, null, 2);
// CORRECT: Buffer.byteLength counts BYTES, not characters (lesson 03-06).
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'));Using body.length here is a classic and very damaging mistake: 'Bóveda' has 6 characters and 7 bytes in UTF-8. Announcing one byte too few makes the client truncate the response and fail the JSON.parse, with an incomprehensible message.
Cache-Control says how long the response may be stored. In an API of live data, no-store; in a static file, seconds or years (04-04). Location points to where the resource is: mandatory in a 201 (where the created thing ended up) and in a 3xx (where to go).
src/server/responses.js
src/server/responses.jsWriting statusCode, setHeader and end in every branch is repetitive and, above all, it is where inconsistencies creep in: one route forgetting the charset, another returning the error as plain text. Let's centralize it.
// src/server/responses.js
// Helpers for building consistent HTTP responses across the whole server.
const JSON_UTF8 = 'application/json; charset=utf-8';
const TEXT_UTF8 = 'text/plain; charset=utf-8';
// Writes an already serialized body with its Content-Type and exact length.
function sendBody(res, status, type, body, headers = {}) {
if (res.headersSent) {
console.error('[responses] tried to respond twice; ignoring');
return;
}
res.statusCode = status;
res.setHeader('Content-Type', type);
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'));
for (const [name, value] of Object.entries(headers)) res.setHeader(name, value);
res.end(body);
}
function sendJson(res, status, data, headers = {}) {
sendBody(res, status, JSON_UTF8, JSON.stringify(data, null, 2), headers);
}
function sendText(res, status, text, headers = {}) {
sendBody(res, status, TEXT_UTF8, text, headers);
}
// 204 and 304 carry NO body: sending one violates the spec and confuses proxies.
function sendNoContent(res, status = 204, headers = {}) {
if (res.headersSent) return;
res.statusCode = status;
for (const [name, value] of Object.entries(headers)) res.setHeader(name, value);
res.end();
}
// A single error shape for the whole API. Letting the client program against
// "code" is more useful than having a human read the "message".
function sendError(res, status, message, code = 'ERROR', extra = {}) {
sendJson(res, status, { error: { code, message, status, ...extra } });
}
function sendRedirect(res, status, target) {
sendNoContent(res, status, { Location: target });
}
module.exports = { sendJson, sendText, sendError, sendNoContent, sendRedirect };Three deliberate decisions. Every error has the same shape ({ error: { code, message, status } }): a client can program against code, which is stable, instead of against message, which will change. Content-Length is always computed with Buffer.byteLength, never with length. And headersSent is checked in a single place, so a double response pollutes the log but does not take down the process.
- From domain
error.appCode to HTTP status
error.appCode to HTTP statusWe reach the module's key piece. Since Module 2, our domain has thrown errors with their own appCode: SESSION_NOT_FOUND, INSUFFICIENT_CAPACITY, INVALID_QUANTITY. That vocabulary belongs to Escena Viva and knows nothing about HTTP: which is exactly what we want, because SalesManager must be usable from a CLI, from a scheduled job or from an API.
The bridge between the two worlds is a table, and it lives in the HTTP layer, not in the domain:
// src/server/http-errors.js
// Translates the DOMAIN error vocabulary into the HTTP one.
// The domain knows nothing about HTTP; this table is the only thing that knows both.
const STATUS_BY_CODE = {
// 400: malformed or badly typed request
INVALID_QUANTITY: 400, INVALID_PARAMETER: 400, INVALID_JSON: 400,
// 403: forbidden by policy
PATH_NOT_ALLOWED: 403,
// 404: the resource does not exist
EVENT_NOT_FOUND: 404, SESSION_NOT_FOUND: 404,
ORDER_NOT_FOUND: 404, RESOURCE_NOT_FOUND: 404,
// 409: valid request that clashes with the current state
INSUFFICIENT_CAPACITY: 409, INVALID_STATE: 409, ORDER_ALREADY_PAID: 409,
// 422: understood, but the business rules forbid it
ORDER_LIMIT_EXCEEDED: 422,
// 503: we depend on something external that is not there right now
EXTERNAL_SERVICE_DOWN: 503
};
// No known translation -> 500: it is OUR fault and we need to see it.
function statusForError(error) {
return STATUS_BY_CODE[error?.appCode] ?? 500;
}
// A 5xx never reveals internal details to the client, but it IS logged.
function bodyForError(error) {
const status = statusForError(error);
if (status >= 500) {
console.error('[error] unclassified failure:', error);
return { status, code: 'INTERNAL_ERROR', message: 'Internal server error' };
}
return { status, code: error.appCode, message: error.message };
}
module.exports = { statusForError, bodyForError, STATUS_BY_CODE };Why this is the right thing and not an architect's luxury:
- The domain stays uncontaminated.
Event.reservekeeps throwingINSUFFICIENT_CAPACITYwithout knowing the number 409 exists, so it can still be used fromsrc/catalog.jsin the terminal. - The default failure is
500and it is noisy. An unknownappCodemeans somebody invented a new error and forgot to register it here: we want to find out, not to have it silently become a400. 5xxresponses leak nothing. The internal message goes tostderrfor us; the client gets generic text. A stack trace in the response is a gift to anyone hunting for your Node version and your disk paths.- There is one single table. When we reach Express in lesson 06-07, the error handler will change shape, but it will still consult this very file.
Using it is one line:
try {
sendJson(res, 200, await findEvent(id)); // throws EVENT_NOT_FOUND
} catch (error) {
const { status, code, message } = bodyForError(error);
sendError(res, status, message, code);
}In the next lesson that try/catch will stop repeating itself on every route: it will move up once into the router's dispatcher.
- Redirects and the
HEAD method
HEAD methodA redirect is a 3xx with the Location header. The four that matter:
| Code | Name | Method on retry | When |
|---|---|---|---|
| 301 | Moved Permanently | May switch to GET |
The URL changed for good; the browser caches it |
| 302 | Found | May switch to GET |
Temporary move |
| 307 | Temporary Redirect | Preserved | Temporary while preserving a POST |
| 308 | Permanent Redirect | Preserved | Permanent while preserving a POST |
// /event/evt-001 is obsolete: the good path is /events/evt-001
sendRedirect(res, 301, `/events/${id}`);Careful with the 301: browsers store it aggressively and, if you get the target wrong, users will keep going to the bad place even after you fix the server, because they will not even ask you again. When in doubt, 302.
The HEAD method asks for a response identical to GET's but with no body: it is used to check a resource's size or date before downloading it. The good news is that Node handles it almost by itself: if the method is HEAD, it discards whatever body you write and sends only the headers. Even so, being explicit avoids generating useless work:
// Register HEAD alongside GET: same headers, body only if it is a GET.
const body = JSON.stringify(data, null, 2);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'));
res.end(req.method === 'HEAD' ? undefined : body);Note that Content-Length is still sent even though there is no body: it is precisely the piece of data the client came looking for.
Common Mistakes and Tips
- Reading
req.headers['Content-Type']with capitals. It returnsundefinedwith no error. The keys are always lowercase. - Splitting
req.urlwithsplit('?')orsplit('/'). It breaks with encoded parameters, repeated ones and more than one. Usenew URL(req.url, base). - Computing
Content-Lengthwithbody.length. It counts characters, not bytes; with an accent or anñ, the response arrives truncated. UseBuffer.byteLength. - Responding without
return. It is the number one cause ofERR_HTTP_HEADERS_SENT. - Sending a body in a
204or a304. The spec forbids it and some proxies choke on it. - Returning
200with{ "error": ... }inside, or the stack trace in a500. The status is part of the response, and the trace goes to the log, never to the client. - Tip: decide from the start on a single error format for the whole API and never change it. Your clients will appreciate it more than any feature.
- Tip: in
catchblocks that wrap streams, checkres.headersSent; if they already went out, the only honest thing left isres.destroy().
Exercises
Exercise 1: filter by venue and category
Extend the handler so that GET /events accepts ?venue= and ?category= (the latter repeatable) and returns the filtered catalog, in the shape { total, filters, events }. Validate that venue is not empty and respond 400 with code: 'INVALID_PARAMETER' if it is. Check it with curl -s 'http://localhost:3000/events?venue=Sala%20B%C3%B3veda'.
Exercise 2: extending the error table
Add to src/server/http-errors.js the codes CORRUPT_DATA (the catalog JSON is broken: that's our fault) and UNSUPPORTED_FORMAT (the client asks for a format we don't serve). Choose the status for each and justify it. Then write src/server/check-errors.js, a script that walks STATUS_BY_CODE and prints a code | status | family table, plus the count per family.
Exercise 3: correct HEAD and Content-Length
Make the handler answer HEAD /events with the same headers as GET /events but with no body, and check with curl -I that Content-Length matches exactly the number of bytes curl -s ... | wc -c returns. Add a venue with an accent to the catalog and verify that it still adds up.
Solutions
Solution 1. All the information comes from searchParams, and the validation throws with error.appCode so the table can translate it:
const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
const venue = url.searchParams.get('venue');
const categories = url.searchParams.getAll('category');
if (venue !== null && venue.trim() === '') {
const error = new Error('The "venue" parameter cannot be empty');
error.appCode = 'INVALID_PARAMETER';
throw error;
}
const events = (await getCatalog())
.filter((event) => venue === null || event.venue === venue)
.filter((event) => categories.length === 0 || categories.includes(event.category));
sendJson(res, 200, { total: events.length, filters: { venue, categories }, events });With ?venue=Sala%20B%C3%B3veda it returns 1 event (evt-002, Noche de Monologos). Had you used split('='), the comparison would be against 'Sala%20B%C3%B3veda' and the result zero events: a filter that "finds nothing" and looks like a data problem.
Solution 2. CORRUPT_DATA is a 500: the catalog file is our responsibility and the client can do nothing about it, so on top of that we want it logged. UNSUPPORTED_FORMAT is a 406 Not Acceptable, the specific status for "I cannot produce any of the formats you accept"; if you would rather not widen the course's vocabulary, 400 is defensible, but 406 is more precise. The script relies on the family being the first digit:
const { STATUS_BY_CODE } = require('./http-errors.js');
const counts = {};
for (const [code, status] of Object.entries(STATUS_BY_CODE)) {
const family = `${Math.floor(status / 100)}xx`;
counts[family] = (counts[family] ?? 0) + 1;
console.log(`${code.padEnd(24)} | ${status} | ${family}`);
}
console.log(JSON.stringify(counts, null, 2));Solution 3. The key is computing the body the same way for both methods and deciding only at the end whether to send it:
const body = JSON.stringify(data, null, 2);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'));
res.end(req.method === 'HEAD' ? undefined : body);curl -I (which sends HEAD) and curl -s ... | wc -c must give the same number. When you add a venue with an accent, Buffer.byteLength grows more than the character count —each accented character takes two bytes in UTF-8—, and that is where you see why body.length would have lied.
Conclusion
You now know how to listen and answer properly. req is an IncomingMessage which, besides method, headers —always lowercase—, httpVersion and socket.remoteAddress, is a readable stream whose body may still be travelling. Its url is not a complete URL but path plus query, and the only sensible way to interpret it is new URL(req.url, base) with searchParams: it decodes percent-encoding, supports repeated keys with getAll and does not break on ?venue=Sala%20B%C3%B3veda.
res is a ServerResponse and a writable stream, with an inviolable order: status and headers first, body afterwards, end() always. From that comes the module's most frequent error, ERR_HTTP_HEADERS_SENT, cured by a simple rule: responding means finishing, and you respond only once. You have the complete catalog of statuses the course will use, with the borders people get wrong most often —400 versus 422, 401 versus 403, 404 versus 409— and the essential headers, including the discipline of computing Content-Length with Buffer.byteLength and not with String.length.
And Escena Viva takes away two modules that will stay with it to the end: src/server/responses.js, with sendJson, sendText, sendError, sendNoContent and sendRedirect, all sharing a single error format; and src/server/http-errors.js, with the table that maps error.appCode to an HTTP status —EVENT_NOT_FOUND to 404, INSUFFICIENT_CAPACITY to 409, INVALID_QUANTITY to 400— and a noisy 500 by default for whatever we cannot classify.
We are missing the obvious: deciding which handler serves each request. In the next lesson, Manual Routing, we will start with the naive if/else, see exactly where it breaks, and build src/server/router.js with a route table, /events/:id-style patterns compiled into regular expressions, parameter extraction, 405 responses with an Allow header and a central try/catch that will use the table you have just written.
Node.js Course: From Beginner to Advanced
Module 1: Introduction to Node.js
- What Is Node.js?
- Installing and Setting Up the Environment
- Your First Node.js Program
- The Node.js REPL
- Modern JavaScript for Node.js
- The Course Project: the Escena Viva Platform
Module 2: Core Concepts
- Node.js Architecture
- The Event Loop
- Callbacks and Asynchronous Programming
- Promises and async/await
- Events and EventEmitter
- CommonJS Modules and require()
- ES Modules and Interoperability
Module 3: File System and I/O
- Reading and Writing Files
- The fs Module in Depth
- Cross-Platform Paths with the path Module
- Working with Streams
- Transform Streams and pipeline
- Buffers and Binary Data
Module 4: HTTP and Web Servers
- Creating a Simple HTTP Server
- Handling Requests and Responses
- Manual Routing
- Serving Static Files
- Receiving Data: Request Bodies and JSON
- Consuming External APIs from Node.js
Module 5: NPM and Package Management
- Introduction to NPM and package.json
- Installing and Using Packages
- Semantic Versioning and package-lock
- npm Scripts and Project Automation
- Creating and Publishing Packages
- Dependency Security and Maintenance
Module 6: The Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Routing in Express
- Middleware
- Essential Third-Party Middleware
- Input Data Validation
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- CRUD Operations
- Relationships, Population and Advanced Queries
- Using SQL Databases with Sequelize
- Migrations, Transactions and Seed Data
Module 8: Authentication and Authorization
- Introduction to Authentication
- User Registration and Password Hashing
- Sessions and Cookies with Passport.js
- Authentication with JWT
- Role-Based Access Control
- API Security Best Practices
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Test Doubles with Sinon
- Integration Testing
- Coverage and Test Automation
- Debugging Node.js Applications
Module 10: Advanced Topics
- The Cluster Module
- Worker Threads
- Caching and Job Queues with Redis
- Performance Optimization
- Building RESTful APIs
- GraphQL with Node.js
Module 11: Deployment and DevOps
- Configuration and Environment Variables
- Logging and Monitoring in Production
- Using PM2 for Process Management
- Packaging with Docker
- Deploying to Heroku and Other PaaS
- Continuous Integration and Deployment
