Forget Express.js — opt for these alternatives instead
Node.js offers some powerful primitives when it comes to building HTTP servers. By default, you get a function that runs every time an HTTP request has been received by the server. The proverbial server example that parses an incoming POST request containing a JSON body looks a bit like this:
const http = require('http');
const server = http.createServer((req, res) => {
// This function is called once the headers have been received
res.setHeader('Content-Type', 'application/json');
if (req.method !== 'POST' || req.url !== '/user') {
res.statusCode = 405;
res.end('{"error":"METHOD_NOT_ALLOWED"}');
return;
}
let body = '';
req.on('data', (data) => {
// This function is called as chunks of body are received
body += data;
});
req.on('end', () => {
// This function is called once the body has been fully received
let parsed;
try {
parsed = JSON.parse(body);
} catch (e) {
res.statusCode = 400;
res.end('{"error":"CANNOT_PARSE"}');
}
res.end(JSON.stringify({
error: false,
username: parsed.username
}));
});
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});By default, Node.js allows us to run a function whenever any request is received. There is no built-in router based on paths. Node.js does perform some basic parsing — for example, parsing the incoming HTTP message and extracting different components like the path, header pairs, encoding (Gzip and SSL), etc.
However, the need for higher-level functionality means that we usually have to reach for a web framework. For example, if a multipart/form-data or application/x-www-form-urlencoded the request is received, we need to use a module to handle decoding the content for us. If we want to simply route requests based on pattern matching and HTTP methods, we’ll need either a module — or, often, a full web framework — to handle this for us.
That’s where tools like Express.js come into play.
Meet Express.js
Express.js fairly early became the go-to framework for building web applications using Node.js. It scratched an itch that many developers had: it provided a nice syntax for routing HTTP requests, it provided a standardized interface for building out middleware, and it did so using the familiar callback pattern embraced by the core Node.js APIs and most of the npm ecosystem.
Express.js became so popular that it’s almost ubiquitously associated with Node.js — much like when we read about the language Ruby, we’re already conjuring up thoughts of the framework Rails. In fact, Express.js and Node.js are members of the popular MEAN and MERN stack acronyms.
Let’s take a look at what our previous example might look like when we bring Express.js into the picture:
const express = require('express');
const app = express();
app.post('/user', (req, res) => {
// This function is called once the headers have been received
let body = '';
req.on('data', (data) => {
// This function is called as chunks of body are received
body += data;
});
req.on('end', () => {
// This function is called once the body has been fully received
let parsed;
try {
parsed = JSON.parse(body);
} catch (e) {
res.statusCode = 400;
res.json({
error: 'CANNOT_PARSE'
});
}
res.json({
error: false,
username: parsed.username
});
});
});
app.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});In this example, we see that things get a little nicer. We’re able to specifically state the method and path we want to match by using app.post('/user'). This is much simpler than writing a big branching statement within the handler.
We’re also given some other niceties. Consider the method: this not only serializes an object into its JSON equivalent, but it also sets the appropriate Content-Type the header for us!
However, Express.js still gives us the same paradigm that we get when using the built-in http module; we’re still calling methods on req and res objects, for example.
An ideal example
Let’s take a step back and look at what an ideal example of an HTTP server might look like. Routing is desirable, and Express.js has a powerful routing syntax (it supports dynamic routing patterns, for instance). However, the code that runs within the controller function is where we really want to clean things up.
In the above example, we’re doing a lot of work with asynchronous code. The request object is an Event Emitter that emits two events we care about, namely data and end. But, really, we often just want the ability to convert an HTTP request into a JSON object that we can easily extract values from.
Also, we’re given both a request (req) and a response (res) object. The req the object makes sense — it contains information about the request we’re receiving. But does the really make all that much sense? We only want to provide a result from our controller function as a reply.
With synchronous functions, it’s simple to receive a result from a function call: just return the value. We can do the same thing if we make use of functions. By returning a call to an async function, the controller function can resolve a value that ultimately represents the response we intend for the consumer to receive.
Let’s look at an example of this:
const server = someCoolFramework();
server.post('/user', async (req) => {
let parsed;
try {
parsed = await req.requestBodyJson();
} catch (e) {
return [400, {
error: 'CANNOT_PARSE'
}];
}
return {
error: false,
username: parsed.username
};
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});There are a few concepts going on in this idealized example of ours. First, we’re maintaining the existing router syntax used by Express.js because it’s pretty solid. Second, our object provides a helper for converting an incoming request into JSON.
The third feature is that we’re able to provide a representation of the response by simply returning a result. Since JavaScript doesn’t support tuples, we’re essentially recreating one by using an array. So with this fictional example, a returned string could be sent directly to the client as a body, a returned array can be used to represent the status code and the body (and perhaps a third parameter for metadata like headers), and a returned object can be converted into its JSON representation.
Comments
Post a Comment