Node.js — Anatomy of an HTTP Transaction (2024)

The purpose of this guide is to impart a solid understanding of the process ofNode.js HTTP handling. We'll assume that you know, in a general sense, how HTTPrequests work, regardless of language or programming environment. We'll alsoassume a bit of familiarity with Node.js EventEmitters and Streams.If you're not quite familiar with them, it's worth taking a quick read throughthe API docs for each of those.

Create the Server

Any node web server application will at some point have to create a web serverobject. This is done by using createServer.

const http = require('node:http');const server = http.createServer((request, response) => { // magic happens here!});

The function that's passed in to createServer is called once for everyHTTP request that's made against that server, so it's called the requesthandler. In fact, the Server object returned by createServer is anEventEmitter, and what we have here is just shorthand for creating aserver object and then adding the listener later.

const server = http.createServer();server.on('request', (request, response) => { // the same kind of magic happens here!});

When an HTTP request hits the server, node calls the request handler functionwith a few handy objects for dealing with the transaction, request andresponse. We'll get to those shortly.

In order to actually serve requests, the listen method needs to be calledon the server object. In most cases, all you'll need to pass to listen isthe port number you want the server to listen on. There are some other optionstoo, so consult the API reference.

Method, URL and Headers

When handling a request, the first thing you'll probably want to do is look atthe method and URL, so that appropriate actions can be taken. Node.js makes thisrelatively painless by putting handy properties onto the request object.

const { method, url } = request;

The request object is an instance of IncomingMessage.

The method here will always be a normal HTTP method/verb. The url is thefull URL without the server, protocol or port. For a typical URL, this meanseverything after and including the third forward slash.

Headers are also not far away. They're in their own object on request calledheaders.

const { headers } = request;const userAgent = headers['user-agent'];

It's important to note here that all headers are represented in lower-case only,regardless of how the client actually sent them. This simplifies the task ofparsing headers for whatever purpose.

If some headers are repeated, then their values are overwritten or joinedtogether as comma-separated strings, depending on the header. In some cases,this can be problematic, so rawHeaders is also available.

Request Body

When receiving a POST or PUT request, the request body might be important toyour application. Getting at the body data is a little more involved thanaccessing request headers. The request object that's passed in to a handlerimplements the ReadableStream interface. This stream can be listened to orpiped elsewhere just like any other stream. We can grab the data right out ofthe stream by listening to the stream's 'data' and 'end' events.

The chunk emitted in each 'data' event is a Buffer. If you know it'sgoing to be string data, the best thing to do is collect the data in an array,then at the 'end', concatenate and stringify it.

let body = [];request .on('data', chunk => { body.push(chunk); }) .on('end', () => { body = Buffer.concat(body).toString(); // at this point, `body` has the entire request body stored in it as a string });

This may seem a tad tedious, and in many cases, it is. Luckily,there are modules like concat-stream and body on npm which canhelp hide away some of this logic. It's important to have a good understandingof what's going on before going down that road, and that's why you're here!

A Quick Thing About Errors

Since the request object is a ReadableStream, it's also anEventEmitter and behaves like one when an error happens.

An error in the request stream presents itself by emitting an 'error' eventon the stream. If you don't have a listener for that event, the error will bethrown, which could crash your Node.js program. You should therefore add an'error' listener on your request streams, even if you just log it andcontinue on your way. (Though it's probably best to send some kind of HTTP errorresponse. More on that later.)

request.on('error', err => { // This prints the error message and stack trace to `stderr`. console.error(err.stack);});

There are other ways of handling these errors such asother abstractions and tools, but always be aware that errors can and do happen,and you're going to have to deal with them.

What We've Got so Far

At this point, we've covered creating a server, and grabbing the method, URL,headers and body out of requests. When we put that all together, it might looksomething like this:

const http = require('node:http');http .createServer((request, response) => { const { headers, method, url } = request; let body = []; request .on('error', err => { console.error(err); }) .on('data', chunk => { body.push(chunk); }) .on('end', () => { body = Buffer.concat(body).toString(); // At this point, we have the headers, method, url and body, and can now // do whatever we need to in order to respond to this request. }); }) .listen(8080); // Activates this server, listening on port 8080.

If we run this example, we'll be able to receive requests, but not respondto them. In fact, if you hit this example in a web browser, your request wouldtime out, as nothing is being sent back to the client.

So far we haven't touched on the response object at all, which is an instanceof ServerResponse, which is a WritableStream. It contains manyuseful methods for sending data back to the client. We'll cover that next.

HTTP Status Code

If you don't bother setting it, the HTTP status code on a response will alwaysbe 200. Of course, not every HTTP response warrants this, and at some pointyou'll definitely want to send a different status code. To do that, you can setthe statusCode property.

response.statusCode = 404; // Tell the client that the resource wasn't found.

There are some other shortcuts to this, as we'll see soon.

Setting Response Headers

Headers are set through a convenient method called setHeader.

response.setHeader('Content-Type', 'application/json');response.setHeader('X-Powered-By', 'bacon');

When setting the headers on a response, the case is insensitive on their names.If you set a header repeatedly, the last value you set is the value that getssent.

Explicitly Sending Header Data

The methods of setting the headers and status code that we've already discussedassume that you're using "implicit headers". This means you're counting on nodeto send the headers for you at the correct time before you start sending bodydata.

If you want, you can explicitly write the headers to the response stream.To do this, there's a method called writeHead, which writes the statuscode and the headers to the stream.

response.writeHead(200, { 'Content-Type': 'application/json', 'X-Powered-By': 'bacon',});

Once you've set the headers (either implicitly or explicitly), you're ready tostart sending response data.

Sending Response Body

Since the response object is a WritableStream, writing a response bodyout to the client is just a matter of using the usual stream methods.

response.write('<html>');response.write('<body>');response.write('<h1>Hello, World!</h1>');response.write('</body>');response.write('</html>');response.end();

The end function on streams can also take in some optional data to send as thelast bit of data on the stream, so we can simplify the example above as follows.

response.end('<html><body><h1>Hello, World!</h1></body></html>');

It's important to set the status and headers before you startwriting chunks of data to the body. This makes sense, since headers come beforethe body in HTTP responses.

Another Quick Thing About Errors

The response stream can also emit 'error' events, and at some point you'regoing to have to deal with that as well. All of the advice for request streamerrors still applies here.

Put It All Together

Now that we've learned about making HTTP responses, let's put it all together.Building on the earlier example, we're going to make a server that sends backall of the data that was sent to us by the user. We'll format that data as JSONusing JSON.stringify.

const http = require('node:http');http .createServer((request, response) => { const { headers, method, url } = request; let body = []; request .on('error', err => { console.error(err); }) .on('data', chunk => { body.push(chunk); }) .on('end', () => { body = Buffer.concat(body).toString(); // BEGINNING OF NEW STUFF response.on('error', err => { console.error(err); }); response.statusCode = 200; response.setHeader('Content-Type', 'application/json'); // Note: the 2 lines above could be replaced with this next one: // response.writeHead(200, {'Content-Type': 'application/json'}) const responseBody = { headers, method, url, body }; response.write(JSON.stringify(responseBody)); response.end(); // Note: the 2 lines above could be replaced with this next one: // response.end(JSON.stringify(responseBody)) // END OF NEW STUFF }); }) .listen(8080);

Echo Server Example

Let's simplify the previous example to make a simple echo server, which justsends whatever data is received in the request right back in the response. Allwe need to do is grab the data from the request stream and write that data tothe response stream, similar to what we did previously.

const http = require('node:http');http .createServer((request, response) => { let body = []; request .on('data', chunk => { body.push(chunk); }) .on('end', () => { body = Buffer.concat(body).toString(); response.end(body); }); }) .listen(8080);

Now let's tweak this. We want to only send an echo under the followingconditions:

  • The request method is POST.
  • The URL is /echo.

In any other case, we want to simply respond with a 404.

const http = require('node:http');http .createServer((request, response) => { if (request.method === 'POST' && request.url === '/echo') { let body = []; request .on('data', chunk => { body.push(chunk); }) .on('end', () => { body = Buffer.concat(body).toString(); response.end(body); }); } else { response.statusCode = 404; response.end(); } }) .listen(8080);

By checking the URL in this way, we're doing a form of "routing".Other forms of routing can be as simple as switch statements or as complex aswhole frameworks like express. If you're looking for something that doesrouting and nothing else, try router.

Great! Now let's take a stab at simplifying this. Remember, the request objectis a ReadableStream and the response object is a WritableStream.That means we can use pipe to direct data from one to the other. That'sexactly what we want for an echo server!

const http = require('node:http');http .createServer((request, response) => { if (request.method === 'POST' && request.url === '/echo') { request.pipe(response); } else { response.statusCode = 404; response.end(); } }) .listen(8080);

Yay streams!

We're not quite done yet though. As mentioned multiple times in this guide,errors can and do happen, and we need to deal with them.

To handle errors on the request stream, we'll log the error to stderr and senda 400 status code to indicate a Bad Request. In a real-world application,though, we'd want to inspect the error to figure out what the correct status codeand message would be. As usual with errors, you should consult theError documentation.

On the response, we'll just log the error to stderr.

const http = require('node:http');http .createServer((request, response) => { request.on('error', err => { console.error(err); response.statusCode = 400; response.end(); }); response.on('error', err => { console.error(err); }); if (request.method === 'POST' && request.url === '/echo') { request.pipe(response); } else { response.statusCode = 404; response.end(); } }) .listen(8080);

We've now covered most of the basics of handling HTTP requests. At this point,you should be able to:

  • Instantiate an HTTP server with a request handler function, and have it listenon a port.
  • Get headers, URL, method and body data from request objects.
  • Make routing decisions based on URL and/or other data in request objects.
  • Send headers, HTTP status codes and body data via response objects.
  • Pipe data from request objects and to response objects.
  • Handle stream errors in both the request and response streams.

From these basics, Node.js HTTP servers for many typical use cases can beconstructed. There are plenty of other things these APIs provide, so be sure toread through the API docs for EventEmitters, Streams, and HTTP.

PrevHow to publish a Node-API packageNextABI Stability

Node.js — Anatomy of an HTTP Transaction (2024)

References

Top Articles
Latest Posts
Article information

Author: Nicola Considine CPA

Last Updated:

Views: 5917

Rating: 4.9 / 5 (49 voted)

Reviews: 88% of readers found this page helpful

Author information

Name: Nicola Considine CPA

Birthday: 1993-02-26

Address: 3809 Clinton Inlet, East Aleisha, UT 46318-2392

Phone: +2681424145499

Job: Government Technician

Hobby: Calligraphy, Lego building, Worldbuilding, Shooting, Bird watching, Shopping, Cooking

Introduction: My name is Nicola Considine CPA, I am a determined, witty, powerful, brainy, open, smiling, proud person who loves writing and wants to share my knowledge and understanding with you.