Express.js, at its core, is a minimalist web application framework for Node.js. It’s designed to provide a robust set of features for web and mobile applications, without obscuring Node.js’s own features. Think of it as a thin layer on top of Node.js that makes building web servers and APIs significantly easier and more structured.
Let’s see it in action. Imagine a simple Express app that responds with "Hello, World!" to any request on the root path.
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
When you run this Node.js script, Express sets up an HTTP server. The app.get('/', ...) line defines a route handler. This means that whenever an HTTP GET request comes in for the path /, the function provided to app.get will be executed. Inside that function, req (request) and res (response) objects are available. res.send('Hello, World!') tells Express to send the string "Hello, World!" back to the client that made the request. The app.listen(port, ...) part starts the server, making it listen for incoming connections on the specified port.
Now, let’s zoom out and understand the problem Express solves. Building a web server from scratch with Node.js’s built-in http module involves a lot of manual work: parsing request URLs, handling different HTTP methods (GET, POST, etc.), managing request bodies, and routing requests to the correct logic. Express abstracts this away. It provides a clear, convention-over-configuration approach to define routes, apply middleware, and send responses. Middleware, in particular, is a powerful concept. It’s essentially functions that have access to the request and response objects, and can execute code, make changes to them, or end the request-response cycle. This allows for modularity in handling common tasks like logging, authentication, or parsing request bodies.
The fundamental building blocks in Express are:
- Router: The core of Express. It handles incoming requests and directs them to the appropriate handler based on the request method and URL path.
- Middleware: Functions that can process requests and responses. They form a pipeline, and each middleware can either pass the request along to the next one or terminate the cycle.
- Request (req) and Response (res) Objects: These objects provide access to the incoming request data and methods to construct and send the outgoing response.
Consider a slightly more complex scenario: adding a simple logger middleware to our "Hello, World!" app.
const express = require('express');
const app = express();
const port = 3000;
// Middleware to log requests
const requestLogger = (req, res, next) => {
console.log(`${req.method} ${req.url} at ${new Date().toISOString()}`);
next(); // Pass control to the next middleware/handler
};
app.use(requestLogger); // Apply the middleware to all routes
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
Here, requestLogger is a middleware function. When a request comes in, app.use(requestLogger) ensures this function runs first. It logs the HTTP method, URL, and timestamp to the console. The crucial part is next(). This function, provided by Express, signals that the current middleware has finished its work and the request should be passed to the next middleware in the stack, or to the route handler if there are no more middleware. If next() is not called, the request will hang, waiting for a response that will never come.
The true power of middleware lies in its composability and the ability to modify req and res objects. For instance, a middleware can parse JSON request bodies:
const express = require('express');
const app = express();
const port = 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
app.post('/data', (req, res) => {
console.log('Received data:', req.body); // req.body is populated by express.json()
res.json({ message: 'Data received successfully', data: req.body });
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
When you send a POST request with a JSON body to /data, express.json() (a built-in middleware) intercepts the request. It parses the incoming JSON string from the request body and attaches the resulting JavaScript object to req.body. Without this middleware, req.body would be undefined. This pattern of middleware transforming requests before they reach your core route logic is fundamental to Express development.
A common pattern you’ll see is defining routes in separate files and then mounting them onto the main Express application. This is achieved using express.Router().
// routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.send('List of users');
});
router.post('/', (req, res) => {
res.send('Create a new user');
});
module.exports = router;
// app.js
const express = require('express');
const userRoutes = require('./routes/users'); // Import the router
const app = express();
const port = 3000;
app.use('/users', userRoutes); // Mount the user routes at '/users'
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
Here, routes/users.js defines a router specifically for user-related endpoints. In app.js, app.use('/users', userRoutes) tells Express that any request starting with /users should be handled by the userRoutes router. So, a GET request to /users will be handled by userRoutes.get('/', ...), and a POST request to /users will be handled by userRoutes.post('/', ...). This modularity keeps your main application file clean and organized, especially as your application grows.
The one thing most people don’t realize about middleware order is how it directly impacts the request lifecycle and the availability of data. For example, if you try to access req.body in a route handler before you’ve used express.json() or express.urlencoded(), it will be undefined. The order in which you app.use() your middleware functions determines the sequence in which they are executed for any given request. Later middleware or route handlers only see the request after all preceding middleware has had a chance to process it.
Understanding how to effectively use middleware, manage routing, and leverage Express’s built-in helpers is key to building scalable and maintainable Node.js applications. The next logical step is to explore asynchronous operations and error handling within Express applications.