Express.js is the most-used HTTP server and middleware platform for Node.js. Let’s take a hands-on look at what it brings to the table.
Request handling with Express.js
Handling requests over the Internet is one of the most frequent and common tasks in software development. An HTTP server like Express.js lets you define where requests come in, how they are parsed, and how a response is formulated. Its enormous and lasting popularity is a testament to how effectively Express.js handles these tasks.
When you start up an HTTP server on a machine (say, a virtual machine in the cloud), the first thing it needs to know is what port it will “listen” on. Ports are a part of the Transmission Control Protocol (TCP) that runs beneath HTTP. Ports allow many different services to run on the same machine, each one binding to its own unique number.
As an example, to listen on port 3000 using Express, we would do the following:
const express = require('express');
const app = express();
app.listen(3000, () => {
console.log(`Server listening on port ${port}`);
});
By itself, this call doesn’t do much. It requires the Express module, which it uses to create an app
object. It then uses the app.listen()
function to listen on port 3000 and log a message when done.
We also need an endpoint, which is a specific place where requests are handled. For that, we need to add a handler, like so:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello, InfoWorld!');
});
app.listen(3000, () => {
console.log(`Server listening on port 3000`);
});
The app.get()
function corresponds to the HTTP GET
method. Whenever a GET
request arrives at the defined path—in this case, the root path at /
—the defined callback function is executed.
Within the callback function, we receive two objects, req
and res
, representing the application’s request and response. (Using these names for the arguments is conventional but not required.) These give us everything we need to understand what the request contains and formulate a response to send back.
In this case, we use res.send()
to fire off a simple string response.
To run this simple server, we’ll need Node and NPM installed. (If you don’t already have these packages, you can install them using the NVM tool.) Once Node is installed, we can create a new file called server.js
, put the above file listing in it, install Express.js, and run it like so:
$ npm add express
added 65 packages, and audited 66 packages in 2s
13 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
$ node server.js
Server listening on port 3000
Now, if you visit localhost:3000
in your browser, you’ll see a greeting.
Express as middleware
Although endpoints give us everything we need to field requests, there are also many occasions when we need to run logic on them. We can do this in a blanket fashion, running the same logic on all the requests, or only on a portion of them. A perennial example is security, which requires filtering many requests.
If we wanted to log all the requests coming into our server, we’d do this:
const express = require('express');
const app = express();
function logger(req, res, next) {
console.error(`Incoming request: ${req.method} ${req.url}`);
next();
}
app.use(logger);
app.get('/', (req, res) => {
res.send('Hello, InfoWorld!');
});
app.listen(3000, () => {
console.log(`Server listening on port 3000`);
});
This sample is the same as before, except we’ve added a new function called logger
and passed it to the server engine with app.use()
. Just like an endpoint, the middleware function receives a request and response object, but it also accepts a third parameter we’ve named next
. Calling the next
function tells the server to continue on with the middleware chain, and then on to any endpoints for the request.
Express endpoints with parameters
Something else every server needs to do is handle parameters in requests. There are a couple kinds of parameters. One is a path parameter, seen here in a simple echo endpoint:
app.get('/echo/:msg', (req, res) => {
const message = req.params.msg;
res.send(`Echoing ${message}`);
});
If you visit this route at localhost:3000/echo/test123
, you’ll get your message echoed back to you. The path parameter called :msg
is identified with the colon variable and then recovered from the req.params.msg
field.
Another kind of parameter is a query (also known as search) parameter, which is defined in the path after a question mark (?) character. We handle queries like so:
app.get('/search', (req, res) => {
const query = req.query.q;
console.log("Searching for: " + query);
})
This URL localhost:3000/search?q=search term
will cause the endpoint to be activated. The string “search term” will then be logged to the console.
Query parameters are broken up into key/value pairs. In our example, we use the q
key, which is short for “query.”
Serving static files and forms with Express
Express also makes it easy to extract the body from a request. This is sent by a form submission in the browser. To set up a form, we can use Express’s support for serving static files. Just add the following line to server.js
, just after the imports:
app.use(express.static('public'));
Now you can create a /public
directory and add a form.html
file:
Simple Form Simple Form
We can also add a new endpoint in server.js
to handle the form submits:
app.post('/submit', (req, res) => {
const formData = req.body;
console.log(formData);
res.send('Form submitted successfully!');
})
Now, if a request comes into /submit
, it’ll be handled by this function, which grabs the form using req.body()
and logs it to the console. We can use the form with the submit button or mock it with a CURL request, like so:
curl -X POST -d "name=John+Doe&email=johndoe@example.com" http://localhost:3000/submit
Then the server will log it:
{
name: 'John Doe',
email: 'johndoe@example.com'
}
This gives you everything to handle form submits, or even AJAX requests that look like them.
Express modules
Using endpoints and middleware, you can cover a wide range of the needs that arise in building web applications. It doesn’t take long before even a modest application can grow unwieldy and require some kind of organization. One step you can take is to use JavaScript modules to extract your routes and middleware into their own related files.
In Express, we use the Router
object to define endpoints in secondary files, then we import those into the main file. For example, if we wanted to move our echo endpoints out of server.js
and into their own module, we could define them in their own echo.js
file:
// echo.js
const express = require('express');
const router = express.Router();
router.get('/echo/:msg', (req, res) => {
const message = req.params.msg;
res.send(`Module is echoing ${message}`);
});
module.exports = router;
This file exposes the same echo endpoint as before, but uses the express.Router()
object to do it in a reusable fashion. We define the endpoint on the router object, then return that as the export from the JavaScript module with module.exports = router;
.
When another file imports that, it can add it to its own routes, like back in our server file:
// server.js
// ... The rest is the same
app.use(logger);
const echoRouter = require('./echo');
app.use(echoRouter);
//... The rest is the same
Here, we import and make use of the echo router with app.use()
. In this way, our externally defined routes are used like a custom middleware plugin. This makes for a highly extensible platform, and it’s nice that the same concept and syntax is used for our own routes and extensions as well as third-party plugins.
Conclusion
Express.js is a popular option for developers in need of an HTTP server, and it’s easy to see why. It’s incredibly handy. Express’s low overhead and flexibility shine on smaller jobs and for quick tasks. As an application grows larger, Express expects the developer to do more of the work of keeping all the parts organized. Other, more structured frameworks, like Next.js, do more of the default organizing for you.
Express.js also runs on Node, which poses a challenge for true concurrency, but you need significant traffic to truly feel its performance limitations. All in all, Express.js is a highly capable and mature platform that can handle almost any requirements you throw at it.
We’ll continue exploring Express.js in my next article, with a look at more advanced features like view templating.