Skip to main content

Command Palette

Search for a command to run...

Getting Started with Express.js: Building Your First Server and Handling Requests

Updated
24 min read
Getting Started with Express.js: Building Your First Server and Handling Requests

Introduction: The Problem That Express Solves

In the previous article, we built a working HTTP server using nothing but Node.js and its built-in http module. If you went through that exercise, you saw exactly what is involved when you work at that level. You had to manually check request.url to figure out what page was being requested. You had to set headers yourself. You had to handle every possible URL with a chain of if-else statements. You had to call response.end() every single time.

It worked. But imagine building an application with fifty different routes, handling different HTTP methods on each route, parsing incoming data from forms or JSON, and managing error responses. The code would become very long, very repetitive, and increasingly difficult to read and maintain.

This is the problem that Express.js was built to solve.

Express is not a replacement for Node.js. It runs on top of Node.js, using all the same underlying mechanisms. What it does is give you a cleaner, more organized way to write server code. It handles a lot of the repetitive work for you and gives you a structured way to define how your server should respond to different requests.

In this article, we are going to look at what Express actually is, why it exists, how it compares to raw Node.js server code, and how to use it to handle GET and POST requests and send responses. We will keep the examples focused and readable so the concepts are clear.


Part 1: What Express.js Actually Is

Express.js is a web application framework for Node.js. The word framework is important here. A framework is not just a collection of helper functions. It is a structured way of building something. It gives you conventions to follow, tools to use, and a pattern for organizing your code.

Express specifically describes itself as a minimal and flexible framework. Minimal means it does not force a lot of opinions on you or include a huge amount of built-in features you did not ask for. Flexible means you can structure your application in different ways and add the pieces you need as you need them.

At its core, Express does a few key things:

It gives you a clean way to define routes. A route is a combination of an HTTP method and a URL path, paired with a function that handles requests to that combination. Instead of a chain of if-else statements checking request.url, you write app.get('/about', ...) and Express calls your function whenever someone sends a GET request to /about.

It simplifies sending responses. Instead of manually setting status codes, writing headers, and calling response.end(), Express gives you convenience methods on the response object that handle these details.

It supports middleware. Middleware is a concept where you can define functions that run on every request (or a subset of requests) before they reach your route handlers. Things like parsing JSON from a request body, logging requests, checking authentication — these are all common middleware tasks. Express makes adding middleware straightforward.

It provides a router for organizing routes. As your application grows, you can split your routes into separate files and modules, keeping your codebase organized.

Express is also by far the most widely used Node.js web framework. It is mature, well-documented, and has an enormous ecosystem of compatible packages built around it. Learning Express is learning the foundation that many other Node.js tools and frameworks are built upon.


Part 2: Express vs Raw Node.js — A Direct Comparison

The best way to understand the value of Express is to look at the same server built two ways: once with raw Node.js and once with Express.

The Raw Node.js Version

Here is a simple server handling three routes, built with just the http module:

const http = require('http');

const server = http.createServer(function(request, response) {

    if (request.method === 'GET' && request.url === '/') {
        response.statusCode = 200;
        response.setHeader('Content-Type', 'text/plain');
        response.end('Welcome to the homepage');

    } else if (request.method === 'GET' && request.url === '/about') {
        response.statusCode = 200;
        response.setHeader('Content-Type', 'text/plain');
        response.end('This is the about page');

    } else if (request.method === 'GET' && request.url === '/contact') {
        response.statusCode = 200;
        response.setHeader('Content-Type', 'text/plain');
        response.end('This is the contact page');

    } else {
        response.statusCode = 404;
        response.setHeader('Content-Type', 'text/plain');
        response.end('Page not found');
    }
});

server.listen(3000, function() {
    console.log('Server running on port 3000');
});

This works, but notice the repetition. Every route requires you to check both the method and the URL. Every route requires you to manually set the status code and the Content-Type header. Every route requires calling response.end(). As routes are added, this grows into a deeply nested, repetitive structure that becomes hard to scan and maintain.

The Express Version

Here is the exact same server built with Express:

const express = require('express');
const app = express();

app.get('/', function(req, res) {
    res.send('Welcome to the homepage');
});

app.get('/about', function(req, res) {
    res.send('This is the about page');
});

app.get('/contact', function(req, res) {
    res.send('This is the contact page');
});

app.listen(3000, function() {
    console.log('Server running on port 3000');
});

Both servers do the same thing. But the Express version is shorter, easier to read, and much easier to extend. Each route is a single, clear declaration. Express handles the method and URL matching for you. The res.send() method handles setting the Content-Type header and calling the equivalent of response.end() for you.

This is what Express is doing: taking the patterns you would write repeatedly and building them into the framework so you can focus on the logic specific to your application.

Part 3: Installing Express

Express is not built into Node.js the way http and fs are. It is a third-party package that you install using npm.

First, create a directory for your project and navigate into it:

mkdir express-basics
cd express-basics

Initialize a new Node.js project. This creates a package.json file that keeps track of your project's details and dependencies:

npm init -y

The -y flag answers yes to all the default prompts so you do not have to step through each one manually. You can look at the generated package.json to see what it contains.

Now install Express:

npm install express

This downloads Express and all its dependencies into a node_modules folder in your project directory. It also adds Express as a dependency in your package.json so that anyone else setting up your project knows it is needed.

After the installation completes, your project directory will look like this:

express-basics/
    node_modules/
    package.json
    package-lock.json

You are ready to start writing code.

One important note: add a .gitignore file if you are using Git, and put node_modules in it. The node_modules folder can contain thousands of files and should not be committed to version control. Anyone who clones your project can run npm install to get those files themselves.


Part 4: Creating Your First Express Server

Create a file called server.js in your express-basics directory:

const express = require('express');

const app = express();
const PORT = 3000;

app.get('/', function(req, res) {
    res.send('Hello from Express');
});

app.listen(PORT, function() {
    console.log('Express server is running on port ' + PORT);
});

Run it:

node server.js

Output:

Express server is running on port 3000

Visit http://localhost:3000 in your browser. You will see:

Hello from Express

Let us go through every line of this so nothing is unclear.

Breaking Down the Server Code

Importing Express

const express = require('express');

This loads the Express package that you installed. The require function looks in node_modules for the express package and returns it.

Creating the Application

const app = express();

express() is a function that creates an Express application. The returned value, which we call app, is the central object you will work with. Everything — defining routes, configuring middleware, starting the server — is done through this object.

Defining a Route

app.get('/', function(req, res) {
    res.send('Hello from Express');
});

app.get() defines a route that handles HTTP GET requests. The first argument is the URL path. The second argument is the handler function — the function that runs when a request matches this route.

The handler function receives two arguments: req (the request object) and res (the response object). These are enhanced versions of the raw request and response objects from Node.js's http module. Express adds many convenient methods and properties to both of them.

res.send() sends a response. Express automatically figures out the Content-Type header based on what you pass. If you pass a string, it sets text/html. If you pass an object or array, it sets application/json and serializes the data as JSON. It also automatically sets the status code to 200 if you have not set it yourself.

Starting the Server

app.listen(PORT, function() {
    console.log('Express server is running on port ' + PORT);
});

app.listen() starts the server and tells it to listen for incoming connections on the specified port. The callback function runs once when the server is ready. This is similar to calling server.listen() in the raw Node.js version, but Express wraps it directly on the app object.


Part 5: Handling GET Requests

GET requests are the most common type of HTTP request. Every time you type a URL into your browser and press Enter, your browser sends a GET request. GET requests are used to retrieve data.

Basic GET Routes

const express = require('express');
const app = express();

app.get('/', function(req, res) {
    res.send('Homepage');
});

app.get('/about', function(req, res) {
    res.send('About page');
});

app.get('/products', function(req, res) {
    res.send('Products page');
});

app.listen(3000, function() {
    console.log('Server running on port 3000');
});

Each app.get() call defines a separate route. Express matches the incoming request URL to these routes and calls the appropriate handler. If no route matches, Express sends a default 404 response automatically.

Sending Different Types of Responses

The res.send() method is flexible, but Express provides more specific methods for different response types.

Sending plain text:

app.get('/text', function(req, res) {
    res.send('This is plain text');
});

Sending JSON:

app.get('/data', function(req, res) {
    res.json({
        name: 'Alice',
        age: 30,
        occupation: 'Engineer'
    });
});

res.json() automatically sets the Content-Type to application/json and converts the JavaScript object to a JSON string. This is the method you will use for API endpoints.

Sending with a specific status code:

app.get('/not-ready', function(req, res) {
    res.status(503).send('Service temporarily unavailable');
});

res.status() sets the HTTP status code. You can chain it with send(), json(), or other response methods.

Sending an HTML response:

app.get('/welcome', function(req, res) {
    const html = `
        <!DOCTYPE html>
        <html>
            <head><title>Welcome</title></head>
            <body>
                <h1>Welcome to Express</h1>
                <p>This HTML is sent from a route handler.</p>
            </body>
        </html>
    `;
    res.send(html);
});

When you pass an HTML string to res.send(), Express sets the Content-Type to text/html automatically.

Route Parameters

Route parameters allow you to capture values from the URL path. They are defined with a colon prefix in the route path:

app.get('/users/:id', function(req, res) {
    const userId = req.params.id;
    res.json({
        message: 'Fetching user with ID: ' + userId,
        userId: userId
    });
});

When someone visits /users/42, Express captures 42 as the value of the id parameter and makes it available through req.params.id.

You can have multiple parameters in a single route:

app.get('/users/:userId/posts/:postId', function(req, res) {
    const userId = req.params.userId;
    const postId = req.params.postId;

    res.json({
        message: 'Fetching post details',
        userId: userId,
        postId: postId
    });
});

A request to /users/5/posts/89 would give you req.params.userId as '5' and req.params.postId as '89'.

Query Parameters

Query parameters appear after the question mark in a URL, like /products?`category=electronics&sort=price. Express parses these automatically and makes them available through req.query:

app.get('/products', function(req, res) {
    const category = req.query.category;
    const sort = req.query.sort;
    const page = req.query.page || 1;

    res.json({
        message: 'Fetching products',
        filters: {
            category: category,
            sort: sort,
            page: page
        }
    });
});

A request to /products?category=electronics&sort=price&page=2 would give you req.query.category as 'electronics', req.query.sort as 'price', and req.query.page as '2'.

A Practical GET Example

Let us build a small set of GET routes that work with a simple in-memory data set:

const express = require('express');
const app = express();

// Simple in-memory data
const users = [
    { id: 1, name: 'Alice Johnson', email: 'alice@example.com', role: 'admin' },
    { id: 2, name: 'Bob Smith', email: 'bob@example.com', role: 'user' },
    { id: 3, name: 'Carol White', email: 'carol@example.com', role: 'user' },
    { id: 4, name: 'David Brown', email: 'david@example.com', role: 'moderator' }
];

// Get all users, with optional role filter
app.get('/users', function(req, res) {
    const roleFilter = req.query.role;

    if (roleFilter) {
        const filtered = users.filter(function(user) {
            return user.role === roleFilter;
        });
        return res.json({ count: filtered.length, data: filtered });
    }

    res.json({ count: users.length, data: users });
});

// Get one specific user by ID
app.get('/users/:id', function(req, res) {
    const userId = parseInt(req.params.id, 10);
    const user = users.find(function(u) { return u.id === userId; });

    if (!user) {
        return res.status(404).json({ error: 'User not found' });
    }

    res.json({ data: user });
});

app.listen(3000, function() {
    console.log('Server running on port 3000');
});

With this server running, these requests all work:

GET /users                  Returns all four users
GET /users?role=user        Returns only users with role 'user'
GET /users/1                Returns Alice's details
GET /users/99               Returns 404 with error message

Part 6: Handling POST Requests

POST requests are used to send data to the server — creating a new user, submitting a form, sending a message. Unlike GET requests, POST requests carry a body that contains the data being sent.

Setting Up Body Parsing

Before you can read the body of a POST request in Express, you need to tell Express how to parse it. Express includes built-in middleware for this.

For JSON bodies (the most common format in APIs):

app.use(express.json());

For form data (when forms are submitted with default HTML encoding):

app.use(express.urlencoded({ extended: true }));

Place these lines near the top of your file, before your routes. The app.use() method registers middleware that runs on every request. These middleware functions read the raw request body, parse it, and attach the result to req.body so your route handlers can access it easily.

If you forget to include these middleware calls and try to read req.body in a POST route, you will get undefined. This is one of the most common beginner mistakes with Express POST handling.

Basic POST Route

const express = require('express');
const app = express();

// Body parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post('/submit', function(req, res) {
    console.log('Received POST request');
    console.log('Request body:', req.body);

    res.json({
        message: 'Data received successfully',
        receivedData: req.body
    });
});

app.listen(3000, function() {
    console.log('Server running on port 3000');
});

To test a POST route, you cannot simply visit a URL in your browser (browsers send GET requests when you type in the address bar). You need a tool that can send POST requests with a body.

Using curl from the terminal:

curl -X POST http://localhost:3000/submit \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com"}'

Using a tool like Postman or Insomnia:

These are graphical applications specifically designed for testing APIs. You can set the request method to POST, the URL, and the body, then send the request and see the response.

When the above curl request is sent to the server, you will see this in the terminal:

Received POST request
Request body: { name: 'Alice', email: 'alice@example.com' }

And the response sent back will be:

{
    "message": "Data received successfully",
    "receivedData": {
        "name": "Alice",
        "email": "alice@example.com"
    }
}

A Practical POST Example

Let us extend the users example to handle creating new users:

const express = require('express');
const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// In-memory storage (in a real app, this would be a database)
const users = [
    { id: 1, name: 'Alice Johnson', email: 'alice@example.com' },
    { id: 2, name: 'Bob Smith', email: 'bob@example.com' }
];

// GET all users
app.get('/users', function(req, res) {
    res.json({ count: users.length, data: users });
});

// POST create a new user
app.post('/users', function(req, res) {
    const name = req.body.name;
    const email = req.body.email;

    // Basic validation
    if (!name || !email) {
        return res.status(400).json({
            error: 'Name and email are required'
        });
    }

    // Create the new user object
    const newUser = {
        id: users.length + 1,
        name: name,
        email: email
    };

    // Add to our in-memory array
    users.push(newUser);

    // Respond with 201 Created and the new user
    res.status(201).json({
        message: 'User created successfully',
        data: newUser
    });
});

app.listen(3000, function() {
    console.log('Server running on port 3000');
});

Now you can:

Send a GET to /users to see all users.

Send a POST to /users with a JSON body like:

{
    "name": "Carol White",
    "email": "carol@example.com"
}

The server validates the input, creates the user, adds it to the array, and responds with 201 Created and the new user's data.

If you send a POST with missing data:

{
    "name": "Incomplete User"
}

The server responds with 400 Bad Request and an error message.

Notice the return before res.status(400).json(...). This is important. Without the return, the function would continue executing after sending the error response, and you would try to send a second response, which causes an error. Always return after sending a response if you want to stop execution of the handler.

Validating POST Data

Real applications need to validate incoming data carefully. Here is a more thorough validation example:

app.post('/register', function(req, res) {
    const name = req.body.name;
    const email = req.body.email;
    const password = req.body.password;
    const age = req.body.age;

    // Collect all validation errors
    const errors = [];

    if (!name || name.trim().length === 0) {
        errors.push('Name is required');
    }

    if (!email || !email.includes('@')) {
        errors.push('A valid email address is required');
    }

    if (!password || password.length < 8) {
        errors.push('Password must be at least 8 characters long');
    }

    if (age !== undefined && (isNaN(age) || age < 18)) {
        errors.push('Age must be a number and at least 18');
    }

    // If there are any errors, return them all at once
    if (errors.length > 0) {
        return res.status(400).json({
            error: 'Validation failed',
            details: errors
        });
    }

    // All validation passed
    res.status(201).json({
        message: 'Registration successful',
        user: {
            name: name.trim(),
            email: email.toLowerCase(),
            age: age
        }
    });
});

This approach collects all validation errors before responding, which gives the client a complete picture of what needs to be fixed rather than reporting one error at a time.


Part 7: Other HTTP Methods

GET and POST are the most common, but HTTP defines other methods that Express handles in the same pattern.

PUT is used to update an existing resource by replacing it entirely:

app.put('/users/:id', function(req, res) {
    const userId = parseInt(req.params.id, 10);
    const updatedName = req.body.name;
    const updatedEmail = req.body.email;

    // In a real app, you would update the database here
    res.json({
        message: 'User updated successfully',
        userId: userId,
        updatedData: {
            name: updatedName,
            email: updatedEmail
        }
    });
});

PATCH is similar to PUT but for partial updates — changing only specific fields:

app.patch('/users/:id', function(req, res) {
    const userId = parseInt(req.params.id, 10);

    // req.body might only contain the fields that should be updated
    res.json({
        message: 'User partially updated',
        userId: userId,
        changes: req.body
    });
});

DELETE is used to remove a resource:

app.delete('/users/:id', function(req, res) {
    const userId = parseInt(req.params.id, 10);

    // In a real app, you would delete from the database here
    res.json({
        message: 'User deleted successfully',
        userId: userId
    });
});

The pattern is always the same: app.method(path, handlerFunction). This consistency is one of Express's strengths.


Part 8: Sending Responses Properly

Express provides several methods for sending responses. Understanding which to use in which situation will make your code clearer and your API more correct.

res.send()

The general-purpose response method. Pass a string, it sends text. Pass an object or array, it sends JSON. Express sets the Content-Type automatically:

res.send('A plain string response');
res.send({ key: 'value' });        // Sends JSON
res.send([1, 2, 3]);               // Sends JSON array

res.json()

Explicitly sends a JSON response. Use this when you specifically intend to send JSON — it is more explicit and therefore clearer to readers of your code:

res.json({ success: true, data: someData });
res.json({ error: 'Something went wrong' });

res.status()

Sets the HTTP status code. Always chain it with a response method:

res.status(200).json({ message: 'OK' });
res.status(201).json({ message: 'Created' });
res.status(400).json({ error: 'Bad Request' });
res.status(401).json({ error: 'Unauthorized' });
res.status(403).json({ error: 'Forbidden' });
res.status(404).json({ error: 'Not Found' });
res.status(500).json({ error: 'Internal Server Error' });

Using correct status codes matters. They tell the client how to interpret the response. A 201 tells the client that something was created. A 400 tells the client they sent something wrong. A 500 tells the client the server had an internal problem.

res.sendStatus()

A shorthand that sets the status code and sends the standard status message as the body:

res.sendStatus(200);   // Sends "OK"
res.sendStatus(404);   // Sends "Not Found"
res.sendStatus(500);   // Sends "Internal Server Error"

This is useful for simple responses where no body content is needed.

A Response Consistency Example

When building an API, consistency in your response format makes it much easier for clients to work with. Here is one approach to a consistent response structure:

const express = require('express');
const app = express();

app.use(express.json());

// Helper functions for consistent responses
function sendSuccess(res, data, statusCode) {
    return res.status(statusCode || 200).json({
        success: true,
        data: data
    });
}

function sendError(res, message, statusCode) {
    return res.status(statusCode || 500).json({
        success: false,
        error: message
    });
}

// Using the helper functions in routes
app.get('/items/:id', function(req, res) {
    const id = parseInt(req.params.id, 10);

    if (isNaN(id)) {
        return sendError(res, 'Invalid ID format', 400);
    }

    // Simulate finding an item
    const item = { id: id, name: 'Sample Item', price: 29.99 };

    if (!item) {
        return sendError(res, 'Item not found', 404);
    }

    sendSuccess(res, item);
});

app.post('/items', function(req, res) {
    if (!req.body.name) {
        return sendError(res, 'Item name is required', 400);
    }

    const newItem = {
        id: Math.floor(Math.random() * 1000),
        name: req.body.name,
        price: req.body.price || 0
    };

    sendSuccess(res, newItem, 201);
});

app.listen(3000, function() {
    console.log('Server running on port 3000');
});

Every response from this server follows the same structure: a success boolean, and either data or error. Clients always know where to look for the information they need.


Part 9: Handling 404 and Errors

Express has a specific way to handle requests that do not match any of your defined routes, and requests that cause errors.

Handling Unmatched Routes

Add this after all your route definitions:

app.use(function(req, res) {
    res.status(404).json({
        error: 'Route not found',
        method: req.method,
        path: req.url
    });
});

app.use() without a path matches every request. Because Express matches routes in the order they are defined, placing this at the end means it only runs when none of the earlier routes matched. It serves as a catch-all for unmatched routes.

Handling Errors

Express has a special type of middleware for error handling. It takes four parameters: err, req, res, and next. Express recognizes a function with four parameters as an error handler:

app.use(function(err, req, res, next) {
    console.error('Error occurred:', err.message);

    res.status(500).json({
        error: 'An internal server error occurred'
    });
});

You trigger this error handler by calling next(error) from within a route handler:

app.get('/risky', function(req, res, next) {
    try {
        // Simulate something that might fail
        const result = someOperationThatMightFail();
        res.json({ data: result });
    } catch (error) {
        next(error);  // Pass the error to the error handler
    }
});

Place the error handling middleware after all routes and after the 404 handler, at the very bottom of your file.


Part 10: A Complete Server Putting It All Together

Here is a complete Express server that combines everything we have covered in this article:

const express = require('express');
const app = express();
const PORT = 3000;

// Middleware for parsing request bodies
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Simple request logger
app.use(function(req, res, next) {
    console.log(req.method + ' ' + req.url + ' - ' + new Date().toISOString());
    next();
});

// In-memory data store
const books = [
    { id: 1, title: 'The Pragmatic Programmer', author: 'Andrew Hunt', year: 1999 },
    { id: 2, title: 'Clean Code', author: 'Robert Martin', year: 2008 },
    { id: 3, title: 'You Do not Know JS', author: 'Kyle Simpson', year: 2015 }
];

let nextId = 4;

// GET all books, with optional year filter
app.get('/books', function(req, res) {
    const yearFilter = req.query.year;

    if (yearFilter) {
        const filtered = books.filter(function(book) {
            return book.year === parseInt(yearFilter, 10);
        });
        return res.json({ count: filtered.length, data: filtered });
    }

    res.json({ count: books.length, data: books });
});

// GET one book by ID
app.get('/books/:id', function(req, res) {
    const bookId = parseInt(req.params.id, 10);

    if (isNaN(bookId)) {
        return res.status(400).json({ error: 'Book ID must be a number' });
    }

    const book = books.find(function(b) { return b.id === bookId; });

    if (!book) {
        return res.status(404).json({ error: 'Book not found' });
    }

    res.json({ data: book });
});

// POST create a new book
app.post('/books', function(req, res) {
    const title = req.body.title;
    const author = req.body.author;
    const year = req.body.year;

    if (!title || !author) {
        return res.status(400).json({
            error: 'Title and author are required'
        });
    }

    const newBook = {
        id: nextId,
        title: title,
        author: author,
        year: year || null
    };

    nextId = nextId + 1;
    books.push(newBook);

    res.status(201).json({
        message: 'Book created successfully',
        data: newBook
    });
});

// PUT update an existing book
app.put('/books/:id', function(req, res) {
    const bookId = parseInt(req.params.id, 10);
    const book = books.find(function(b) { return b.id === bookId; });

    if (!book) {
        return res.status(404).json({ error: 'Book not found' });
    }

    if (!req.body.title || !req.body.author) {
        return res.status(400).json({
            error: 'Title and author are required for a full update'
        });
    }

    book.title = req.body.title;
    book.author = req.body.author;
    book.year = req.body.year || book.year;

    res.json({
        message: 'Book updated successfully',
        data: book
    });
});

// DELETE remove a book
app.delete('/books/:id', function(req, res) {
    const bookId = parseInt(req.params.id, 10);
    const bookIndex = books.findIndex(function(b) { return b.id === bookId; });

    if (bookIndex === -1) {
        return res.status(404).json({ error: 'Book not found' });
    }

    const removedBook = books.splice(bookIndex, 1)[0];

    res.json({
        message: 'Book deleted successfully',
        data: removedBook
    });
});

// 404 handler for unmatched routes
app.use(function(req, res) {
    res.status(404).json({
        error: 'Route not found',
        path: req.url
    });
});

// Global error handler
app.use(function(err, req, res, next) {
    console.error('Unhandled error:', err.message);
    res.status(500).json({
        error: 'An internal server error occurred'
    });
});

app.listen(PORT, function() {
    console.log('Server running on port ' + PORT);
    console.log('Available routes:');
    console.log('  GET    /books');
    console.log('  GET    /books/:id');
    console.log('  POST   /books');
    console.log('  PUT    /books/:id');
    console.log('  DELETE /books/:id');
});

This server handles five routes, validates input, returns appropriate status codes, logs all requests, handles 404 errors for unknown routes, and has a global error handler. It is a complete, working CRUD API for a collection of books.


Summary

Here is a clear recap of everything covered in this article.

Express.js is a minimal, flexible web framework for Node.js. It sits on top of Node.js's built-in http module and gives you a structured, cleaner way to handle HTTP requests and build servers.

Compared to raw Node.js, Express eliminates repetitive code, gives you a clean syntax for defining routes, and provides convenient methods on the request and response objects that handle common tasks automatically.

You install Express with npm install express. You create an application with express(). You start the server with app.listen().

Routes are defined with app.get(), app.post(), app.put(), app.patch(), and app.delete(). Each takes a path and a handler function that receives req and res.

req.params holds URL parameters. req.query holds query parameters. req.body holds the request body for POST and PUT requests, but only after you have set up express.json() or express.urlencoded() middleware.

res.send() sends a response, res.json() sends a JSON response, and res.status() sets the status code. Always return after sending a response to prevent the handler from continuing to execute.

A 404 handler catches unmatched routes. An error handler function with four parameters catches errors passed through next(err).

These are the fundamentals that everything else in Express builds upon.