DEV Community

Cover image for πŸš€ Express.js Fundamentals – Understanding Request/Response Lifecycle and Middleware
Ramanand Thakur
Ramanand Thakur

Posted on

πŸš€ Express.js Fundamentals – Understanding Request/Response Lifecycle and Middleware

Express isn't just a framework that makes routing easier. Its real power comes from the Request/Response Lifecycle and Middleware architecture. By learning these two concepts you'll understand most of Express internals and be able to confidently debug real-world applications.


So far, we've seen that raw Node.js is powerful.

But after writing a few routes, things get messy quickly.

const http = require("http");
const server = http.createServer((req, res) => {
  if (req.url === "/users") {
    res.end("Users");
  } else if (req.url === "/products") {
    res.end("Products");
  } else {
    res.end("404");
  }
});
Enter fullscreen mode Exit fullscreen mode

Imagine maintaining this with:

  • 100 routes
  • Authentication
  • Validation
  • Logging
  • Error handling

That's a nightmare.

Express solves this by introducing:

  • Clean routing
  • Middleware
  • Request parsing
  • Error handling
  • Extensibility

In this article, we'll build a mental model of how Express works internally.


🎯 What You'll Learn

By the end of this guide you'll understand:

βœ… Scaffolding

βœ… Request lifecycle

βœ… Request object

βœ… Response object

βœ… Middleware architecture

βœ… Middleware execution order

βœ… Error handling


πŸ“Œ Prerequisites

You should be comfortable with:

  • Basic JavaScript
  • Node.js fundamentals
  • npm
  • HTTP basics

If you've completed the previous Node.js article, you're ready.


πŸ—οΈ What is Scaffolding?

When we hear the word scaffolding, it often sounds complicated.
In reality, it's a construction term.
When a building is being built, workers first create temporary support structures called scaffolds.
Only then do they start building floors, walls, and rooms.
The same idea applies to software development.

Scaffolding is the process of creating the initial structure of an application before writing actual business logic.
Think of it as preparing the foundation of your project.
Before building APIs, authentication, databases, and features, we first decide:

  • Where routes will live
  • Where middleware will live
  • Where controllers will live
  • Where configuration files will live
  • How the project will be organized

Without scaffolding, projects quickly become difficult to maintain.


Imagine building a house.
You don't start by placing random bricks.
You first create a blueprint.

House
β”‚
β”œβ”€β”€ Living Room
β”œβ”€β”€ Kitchen
β”œβ”€β”€ Bedroom
└── Bathroom
Enter fullscreen mode Exit fullscreen mode

Because each room has a specific purpose.
Similarly, in an Express application:

Application
β”‚
β”œβ”€β”€ Routes
β”œβ”€β”€ Controllers
β”œβ”€β”€ Middleware
β”œβ”€β”€ Models
└── Config
Enter fullscreen mode Exit fullscreen mode

Each folder has a specific responsibility.
This organization makes the project easier to understand and maintain.


🌐 Understanding the Request/Response Lifecycle

πŸ›« Think of It Like Airport Security

When you board a flight:

airport flow

A web request works similarly:

web request flow


πŸ“Š Lifecycle Overview

lifecycle of request

πŸ”₯ What Happens Internally?

Suppose the browser sends:

GET /users/42?page=1 HTTP/1.1
Enter fullscreen mode Exit fullscreen mode

Express processes the request in stages.

Stage 1: Express Creates req and res

Express generates:

req
Enter fullscreen mode Exit fullscreen mode

and

res
Enter fullscreen mode Exit fullscreen mode

objects.

These travel together through the entire lifecycle.

Think of them as a package moving through a conveyor belt.


Stage 2: Middleware Executes

Every registered middleware runs.

app.use(logger);

app.use(auth);
Enter fullscreen mode Exit fullscreen mode

Execution order:

logger
↓
auth
↓
route handler
Enter fullscreen mode Exit fullscreen mode

Order matters.

Always.


Stage 3: Route Matching

Express searches for matching routes.

app.get("/users/:id");
Enter fullscreen mode Exit fullscreen mode

Request:

/users/42
Enter fullscreen mode Exit fullscreen mode

Match found βœ…

Handler executes.


Stage 4: Business Logic Runs

Database queries.

Validation.

Authorization.

Processing.

Everything happens here.


Stage 5: Response Sent

res.json(...)
Enter fullscreen mode Exit fullscreen mode

or

res.send(...)
Enter fullscreen mode Exit fullscreen mode

Browser receives response.

Lifecycle ends.


Now we know the lifecycle of a request - but let's zoom out. Your API receives an HTTP Request with multiple compartments. Let's understand which compartment holds which data, and how Express extracts it.


What is an HTTP Request?

At its core, an HTTP request is just a plain text string sent over a TCP socket from a client (browser, mobile app, cURL) to a server.

That string follows a strict format defined by the HTTP protocol. It looks like this:

POST /api/users?role=admin HTTP/1.1
Host: mywebsite.com
Authorization: Bearer xyz123
Content-Type: application/json
Content-Length: 27

{ "name": "Rama", "age": 25 }
Enter fullscreen mode Exit fullscreen mode

This text is broken down into 4 distinct parts by your web framework (Express, Fastify, etc.). They are:

  1. The Request Line (Method + Path + Query + Protocol)
  2. The Headers (Metadata)
  3. The Body (Data payload)
  4. The Route Parameters (Dynamic segments in the URL path)

Express takes this raw string, parses it into a JavaScript object (req), and gives it to you. Let's decode every piece.


HTTP Request Types (The Methods)

When we say "request types", we mean HTTP Methods (or Verbs). These define the intent of the request.

Method Intent Is it Idempotent?* Has a Body?
GET Fetch data. Yes No (Never)
POST Create a new resource. No Yes
PUT Replace an entire resource entirely. Yes Yes
PATCH Partially update a resource. No Yes
DELETE Remove a resource. Yes No (Usually)
OPTIONS Ask the server what methods are allowed (CORS preflight). Yes No
HEAD Same as GET, but only returns headers, not the body. Yes No

*Idempotent means making the exact same request 1 time or 100 times produces the exact same result on the server (no side effects). If you PUT the same data 10 times, the final state is identical.

Crucial Rule: You access these in Express via req.method.

app.use((req, res, next) => {
  if (req.method === 'GET') console.log('Fetching data');
  if (req.method === 'POST') console.log('Creating data');
  next();
});
Enter fullscreen mode Exit fullscreen mode

πŸ“₯ Understanding the Request Object (req)

The first line of an HTTP request is: METHOD PATH PROTOCOL.

Express splits that PATH into two specific parts: Route Parameters and Query Strings.

1. req.params (Dynamic Values in the URL)

These are named placeholders you define in your route path. They are part of the URL's structural hierarchy.

Suppose your application has the following route:

app.get("/users/:id", (req, res) => {

  console.log(req.params);

});
Enter fullscreen mode Exit fullscreen mode

Notice the :id?

That colon tells Express:

"This part of the URL is dynamic."

Now imagine someone visits:

/users/42
Enter fullscreen mode Exit fullscreen mode

Express automatically extracts the value and stores it inside req.params.

Output:

{
  id: "42"
}
Enter fullscreen mode Exit fullscreen mode

When should you use req.params?

Whenever you're identifying a specific resource.

Examples:

  • /users/42

  • /products/101

  • /orders/9001

Think of it like a house address.

Without the address, the delivery person doesn't know which house to visit.

Similarly, without an ID, your server doesn't know which user, product, or order you're referring to.


2. req.query (Optional Information)

Now consider this URL:

/users?page=2&limit=10
Enter fullscreen mode Exit fullscreen mode

Everything after the ? is called the query string.

Express automatically converts it into:

req.query
Enter fullscreen mode Exit fullscreen mode

Output:

{
  page: "2",
  limit: "10"
}
Enter fullscreen mode Exit fullscreen mode

Unlike route parameters, query parameters are optional.

The same route works perfectly fine without them.

Common use cases

  • Pagination
/users?page=2
Enter fullscreen mode Exit fullscreen mode
  • Searching
/users?search=express
Enter fullscreen mode Exit fullscreen mode
  • Filtering
/products?category=laptops
Enter fullscreen mode Exit fullscreen mode
  • Sorting
/products?sort=price
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Pro Tip: Every value inside req.query is a string. If you need a number, convert it using Number() or parseInt().


req.headers (Metadata About the Request)

Headers are key-value pairs sent before the body. They contain metadata about the request, not the actual data itself.

Common headers you will use daily:

Header Purpose
Authorization Sends credentials (Bearer tokens, Basic Auth).
Content-Type Tells the server how the body is formatted (application/json, multipart/form-data, text/plain).
Accept Tells the server what format the client wants back (application/json, text/html).
Cookie Sends stored session cookies from the browser.
User-Agent Identifies the client's browser/OS.

Express behavior: Express lowercases all header names.

// Incoming header: Authorization: Bearer abc123
console.log(req.headers.authorization); // "Bearer abc123"
console.log(req.headers['content-type']); // "application/json"
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ We'll dive much deeper into request headers when we build authentication and authorization systems later in this series.

req.body (The Actual Data)

When the client wants to create or update something, it usually sends data inside the request body.

For example:

{
  "name": "Raman",
  "role": "Developer"
}
Enter fullscreen mode Exit fullscreen mode

Express makes this data available through:

console.log(req.body);
Enter fullscreen mode Exit fullscreen mode

Output:

{
  name: "Raman",
  role: "Developer"
}
Enter fullscreen mode Exit fullscreen mode

Simple enough...

Well... almost πŸ˜„

By default, Express doesn't know how to read JSON data.

You need to tell it to parse incoming JSON requests by registering the built-in JSON middleware.

app.use(express.json());
Enter fullscreen mode Exit fullscreen mode

Without this middleware:

console.log(req.body);
Enter fullscreen mode Exit fullscreen mode

Output:

undefined
Enter fullscreen mode Exit fullscreen mode

This is probably one of the most common mistakes every beginner and experienced makes.

We'll understand why express.json() is required and how middleware works internally in the Middleware section.

When to use req.body:

  • POST/PUT/PATCH requests where you are sending structured data (e.g., { "email": "ramanand@zohomail.com", "password": "123" }).
  • Critical Rule: A GET request cannot have a req.body. If you put a body on a GET request, most servers and proxies will ignore or reject it.

The "Hidden" Request Properties

Express adds a few extra helpful properties to the req object that you will use:

Property What it gives you
req.ip The IP address of the client.
req.path The URL path without the query string. (e.g., /users/42/posts)
req.hostname The domain name (e.g., mywebsite.com).
req.protocol http or https.
req.cookies Requires middleware (cookie-parser). Gives you parsed cookies.
req.signedCookies Cookies that have been cryptographically signed to prevent tampering.

So when a request hits your server, the data flows into exclusive zones. They do not overlap.

  • URL Path β†’ req.params (e.g., /users/:id)
  • URL Search β†’ req.query (e.g., ?filter=active)
  • HTTP Headers β†’ req.headers (e.g., Authorization)
  • HTTP Body β†’ req.body (e.g., { "name": "John" })

πŸ’‘ Pro Tip: Never put a password or large data in req.params or req.query because URLs are logged by servers and browsers. Always send sensitive data (passwords, credit cards) in the req.body over HTTPS.

Here is how a good developer routes their data:

app.delete('/api/products/:productId', (req, res) => {
  // 1. Identify WHAT to delete (Path param)
  const { productId } = req.params;

  // 2. Identify WHO is deleting (Auth header)
  const userId = req.headers['x-user-id']; // Custom header for internal auth

  // 3. Check for optional admin override (Query param for dev tools)
  const forceDelete = req.query.force === 'true';

  // Logic...
});
Enter fullscreen mode Exit fullscreen mode

πŸ“€ Understanding HTTP Response Status Codes

So far, we've seen how a client sends a request to the server.

But what happens after the server processes that request?

It needs to tell the client what happened.

Did everything go well?

Was something missing?

Did the client send invalid data?

Or did the server itself run into a problem?

That's exactly what HTTP status codes are for.

Think of them as the server's way of saying:

"Here's what happened with your request."


🧠 Status Codes are Organized into Families

Instead of remembering dozens of individual status codes, it's much easier to remember their families.
|Status Code | Meaning|
|--|--|
| 2xx | βœ… Success β€” Everything worked |
| 3xx | πŸ”„ Redirection β€” Go somewhere else |
| 4xx | ❌ Client Error β€” Something is wrong with the request |
| 5xx | πŸ’₯ Server Error β€” Something went wrong on the server |

For most Express applications, you'll spend the majority of your time working with 2xx, 4xx, and 5xx responses.


πŸ“Š Which Status Code Should You Return?

Here's a simple cheat sheet that you'll use in almost every REST API.

Request Success Common Failure Why?
GET 200 OK 404 Not Found Data was returned successfully. If the requested resource doesn't exist, return 404.
POST 201 Created 400 Bad Request A new resource was created. If the client sends invalid data, return 400.
PUT 200 OK or 204 No Content 400 Bad Request The existing resource was updated or replaced.
PATCH 200 OK 400 Bad Request Part of an existing resource was updated.
DELETE 204 No Content or 200 OK 404 Not Found The resource was deleted. If it doesn't exist, return 404.

Don't worry if you don't remember all of these right away.

After building a few APIs, these status codes become second nature.


βœ… 200 OK

This is the most common status code you'll ever return.

It simply means:

"Your request was successful."

For example:

app.get("/users", (req, res) => {

  res.status(200).json([
    {
      id: 1,
      name: "John"
    }
  ]);

});
Enter fullscreen mode Exit fullscreen mode

The server successfully found the users and returned them.


πŸ†• 201 Created

A common beginner mistake is returning 200 OK after creating a new resource.

Technically, it works.

But it's not the best choice.

If your API creates something new, the more appropriate response is:

201 Created
Enter fullscreen mode Exit fullscreen mode

Example:

app.post("/users", (req, res) => {

  const user = {
    id: 101,
    ...req.body
  };

  res.status(201).json(user);

});
Enter fullscreen mode Exit fullscreen mode

The response tells the client:

"Your request didn't just succeedβ€”we actually created something."


πŸ’‘ Pro Tip

Professional APIs often include a Location header pointing to the newly created resource.

res
  .status(201)
  .location("/users/101")
  .json(user);
Enter fullscreen mode Exit fullscreen mode

This makes it easy for clients to immediately fetch the new resource if needed.


🚫 400 Bad Request

This error means:

"I understood your request, but the data you sent is invalid."

Imagine trying to register with:

{
  "email": "not-an-email",
  "password": ""
}
Enter fullscreen mode Exit fullscreen mode

The request reached your server.

Your route executed.

But the data failed validation.

Example:

app.post("/users", (req, res) => {

  if (!req.body.email) {

    return res.status(400).json({

      message: "Email is required"

    });

  }
  // Continue creating user...
});
Enter fullscreen mode Exit fullscreen mode

Notice something important.

The server isn't broken.

The client needs to fix the request.


πŸ” 404 Not Found

This one is probably the easiest to understand.

It simply means:

"The resource you're looking for doesn't exist."

Example:

GET /users/999
Enter fullscreen mode Exit fullscreen mode

If user 999 doesn't exist:

res.status(404).json({

  message: "User not found"

});
Enter fullscreen mode Exit fullscreen mode

Unlike 400, there is nothing wrong with the request format.

The problem is that the requested resource simply doesn't exist.


πŸ’₯ 500 Internal Server Error

This is the status code every developer hopes never reaches production.

It means:

"Something went wrong inside the server."

Maybe:

  • Database connection failed

  • Third-party API is down

  • Unexpected exception occurred

  • Bug in your code

Example:

app.get("/users", async (req, res) => {

  try {

    const users = await database.getUsers();

    res.json(users);

  } catch (error) {

    res.status(500).json({

      message: "Something went wrong."

    });

  }

});
Enter fullscreen mode Exit fullscreen mode

Unlike 400, this isn't the client's fault.

The server failed while processing a perfectly valid request.


πŸ“€ Understanding the Response Object (res)

We've spent quite a bit of time looking at the request object (req)β€”everything the client sends to our server.

But a conversation isn't complete if only one side talks.

After processing the request, the server needs to send something back.

That's where the Response Object (res) comes in.

app.get("/users", (req, res) => {

  // Send a response back to the client

});
Enter fullscreen mode Exit fullscreen mode

Think of it like a conversation.

The browser asks a question.

The server answers it.

Everything the server sends backβ€”whether it's plain text, JSON data, an HTML page, an image, or even an error messageβ€”goes through the res object.

Let's look at the methods you'll use most often.


πŸ’¬ res.send() β€” Send Almost Anything

The simplest way to respond to a client is with res.send().

app.get("/", (req, res) => {

  res.send("Hello, Express!");

});
Enter fullscreen mode Exit fullscreen mode

The browser receives:

Hello, Express!
Enter fullscreen mode Exit fullscreen mode

Simple enough.

But here's the cool part...

res.send() is smart.

It can send:

  • Strings

  • HTML

  • Buffers

  • Objects

  • Arrays

For example:

res.send("<h1>Welcome!</h1>");
Enter fullscreen mode Exit fullscreen mode

renders HTML in the browser.

While this:

res.send({
  name: "John"
});
Enter fullscreen mode Exit fullscreen mode

automatically sends JSON.

Even though res.send() can send almost anything, most modern APIs prefer using res.json() when returning JSON data because it makes your intention much clearer.


πŸ“¦ res.json() β€” Send JSON Responses

Most Express applications today are REST APIs.

And REST APIs almost always communicate using JSON.

Instead of writing:

res.send({
  success: true
});
Enter fullscreen mode Exit fullscreen mode

it's more common to write:

res.json({
  success: true
});
Enter fullscreen mode Exit fullscreen mode

The client receives:

{
  "success": true
}
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, Express automatically does two important things for you:

  • Converts your JavaScript object into JSON.

  • Sets the correct response header.

Content-Type: application/json
Enter fullscreen mode Exit fullscreen mode

You don't have to call JSON.stringify() or manually set the content type.

Express takes care of everything.

That's why you'll see res.json() in almost every Express API.


🚦 res.status() β€” Tell the Client What Happened

Sending data is only half the story.

The client also needs to know whether the request succeeded or failed.

That's exactly what res.status() is for.

For example:

res.status(404);
Enter fullscreen mode Exit fullscreen mode

sets the HTTP status code to 404 Not Found.

In practice, you'll almost always chain it with another response method.

app.get("/users/:id", (req, res) => {

  // Imagine the user doesn't exist...

  res.status(404).json({

    message: "User not found"

  });

});
Enter fullscreen mode Exit fullscreen mode

The client now receives two things:

Status Code

404 Not Found
Enter fullscreen mode Exit fullscreen mode

Response Body

{
  "message": "User not found"
}
Enter fullscreen mode Exit fullscreen mode

This tells the client not only what happened, but also why it happened.


πŸ”— Method Chaining

One thing you'll notice in Express is that response methods are often chained together.

For example:

res.status(201).json(user);
Enter fullscreen mode Exit fullscreen mode

or

res.status(500).send("Internal Server Error");
Enter fullscreen mode Exit fullscreen mode

This works because methods like res.status() return the response object itself, allowing you to immediately call another method.

It makes the code concise and very readable.


🧠 Putting It All Together

Let's build a small example that combines everything we've learned so far.

app.post("/users", (req, res) => {

  const user = {

    id: 101,
    ...req.body

  };

  res
    .status(201)
    .json(user);

});
Enter fullscreen mode Exit fullscreen mode

When the client sends:

{
  "name": "Rama",
  "role": "Developer"
}
Enter fullscreen mode Exit fullscreen mode

The server responds with:

Status

201 Created

Enter fullscreen mode Exit fullscreen mode

Body

{
  "id": 101,
  "name": "Rama",
  "role": "Developer"
}
Enter fullscreen mode Exit fullscreen mode

Notice how the response communicates both the result (a new user was created) and the data (the newly created user).


πŸ”₯ Middleware

Now we're at the heart of Express.

πŸ”₯ Middleware β€” The Superpower Behind Express

So far, we've learned how a request reaches our Express application and how we can send a response back.

But what if you want to:

  • Log every incoming request?

  • Check if a user is authenticated?

  • Parse JSON data?

  • Validate incoming requests?

  • Compress responses?

  • Handle errors?

Surely you don't want to write the same code inside every single route.

That's exactly the problem middleware solves.

In fact, middleware is one of the biggest reasons Express became so popular.


πŸ€” What Exactly is Middleware?

At its core, middleware is simply a function that runs between the incoming request and the outgoing response.

Think of it like a security checkpoint at an airport.

Every passenger has to pass through security before boarding the plane.

Similarly, every request can pass through one or more middleware functions before reaching your route handler.

middleware explained

Instead of handling everything inside your routes, you can move common tasks into reusable middleware.


🚦 The Middleware Pipeline β€” How Every Request Travels Through Express

🧠 Think of Middleware as a Conveyor Belt

Imagine a package moving through a factory.

Before it reaches the customer, it passes through several stations.

conveyer belt as middleware

Each station performs one specific task and then passes the package to the next station.

Middleware works exactly the same way.

Each middleware performs one job and then hands the request to the next middleware in the chain.

That's the entire middleware pipeline.


πŸ› οΈ Anatomy of a Middleware Function

A middleware function always receives three parameters.

const logger = (req, res, next) => {

  console.log(req.method);

  next();

};
Enter fullscreen mode Exit fullscreen mode

Let's understand each one.

Parameter Purpose
req The incoming request from the client
res The response that will eventually be sent back
next A function that tells Express to continue to the next middleware or route

You've already seenreq and res.

The new character here is next().

And it's arguably the most important function in all of Express.


🚦 Understanding next()

Think of next() as saying:

"I'm done with my work. Let the next person continue."

Let's look at a simple example.

const logger = (req, res, next) => {

  console.log("πŸ“₯ Request received");

  next();

};
Enter fullscreen mode Exit fullscreen mode

Every time a request arrives:

  1. The message is logged.

  2. next() is called.

  3. Express moves on to the next middleware (or the route handler).

Simple.

But here's where many get stuck...


😱 What Happens If You Forget next()?

Suppose you accidentally write this:

const logger = (req, res, next) => {

  console.log("πŸ“₯ Request received");

  // Oops...
  // Forgot to call next()

};
Enter fullscreen mode Exit fullscreen mode

Now imagine a request comes in.

The logger runs.

It prints the message.

And then...

Nothing.

Express has no idea what to do next.

It keeps waiting for the middleware to either:

  • send a response, or

  • pass control to the next middleware.

Since neither happened, the request gets stuck forever.

From the browser's perspective, it looks like this:

Loading...
Loading...
Loading...
Loading...
Enter fullscreen mode Exit fullscreen mode

The page never finishes loading because Express is still waiting.

This is one of the most common "silent bugs" beginners encounter.

Your server doesn't crash.

It doesn't throw an error.

The request simply hangs.

Whenever this happens, the first thing to check is:

"Did I forget to call next() or send a response?"


🧩 Why Does This Happen?

Express processes middleware like people standing in a queue.

middleware sequence

Each middleware has exactly two responsibilities:

Option 1: Pass the request forward

next();
Enter fullscreen mode Exit fullscreen mode

This tells Express:

"I'm done. Continue processing the request."


Option 2: End the request

For example:

res.send("Access Denied");
Enter fullscreen mode Exit fullscreen mode

or

res.json({
  message: "Success"
});
Enter fullscreen mode Exit fullscreen mode

or

res.status(401).json({
  message: "Unauthorized"
});
Enter fullscreen mode Exit fullscreen mode

Once a response is sent, the request lifecycle ends.

No other middleware or routes will run after that.


🧩 Types of Middleware

Not every middleware behaves the same way.

Some run for every request.

Some only run on specific routes.

Some are built into Express.

Others come from third-party packages.

Let's look at each type.


🌍 Application Middleware

Application middleware runs for every incoming request.

It's registered using app.use().

app.use((req, res, next) => {

  console.log("πŸ“₯ Incoming request");

  next();

});
Enter fullscreen mode Exit fullscreen mode

Now every request is logged.

Whether someone visits:

/
Enter fullscreen mode Exit fullscreen mode

or

/users
Enter fullscreen mode Exit fullscreen mode

or

/products/101
Enter fullscreen mode Exit fullscreen mode

this middleware runs first.

Application middleware is commonly used for:

  • Logging

  • Authentication

  • Request parsing

  • Security

  • Rate limiting

Think of it as the building's main entrance.

Everyone passes through it.


🎯 Route Middleware

Sometimes you don't want middleware to run for every request.

Maybe only authenticated users should access the dashboard.

Instead of protecting every route, you can protect just one.

app.get(
  "/dashboard",
  authMiddleware,
  dashboardController
);
Enter fullscreen mode Exit fullscreen mode

The execution flow looks like this:

execution flow

If authentication fails, the controller never executes.

This is one of the reasons middleware is so powerful.

It lets you keep your route handlers clean by separating reusable logic.


πŸ› οΈ Built-in Middleware

Some middleware is already included with Express.

You don't need to install anything.

You simply use it.

πŸ“¦ express.json()

Parses incoming JSON requests.

app.use(express.json());
Enter fullscreen mode Exit fullscreen mode

Without this middleware:

console.log(req.body);
Enter fullscreen mode Exit fullscreen mode

returns:

undefined
Enter fullscreen mode Exit fullscreen mode

You've already seen this one earlier.

It's probably the first middleware every Express developer uses.


πŸ“ express.urlencoded()

Parses data submitted from traditional HTML forms.

app.use(
  express.urlencoded({
    extended: true
  })
);
Enter fullscreen mode Exit fullscreen mode

Without it, form data won't appear inside req.body.


πŸ“‚ express.static()

Serves static files like:

  • HTML

  • CSS

  • JavaScript

  • Images

  • Fonts

app.use(express.static("public"));
Enter fullscreen mode Exit fullscreen mode

Now a file like:

public/logo.png
Enter fullscreen mode Exit fullscreen mode

becomes available at:

http://localhost:3000/logo.png
Enter fullscreen mode Exit fullscreen mode

No route required.

Express serves it automatically.


πŸ“¦ Third-Party Middleware

The Express community has built thousands of reusable middleware packages.

Instead of solving common problems yourself, you simply install them.

Some of the most popular ones include:

πŸ›‘οΈ Helmet
app.use(helmet());
Enter fullscreen mode Exit fullscreen mode

Adds several HTTP security headers to make your application safer.


🌍 CORS
app.use(cors());
Enter fullscreen mode Exit fullscreen mode

Allows your frontend and backend to communicate even when they're running on different domains or ports.

We'll cover CORS in detail later because it's an important topic on its own.


πŸ“ Morgan
app.use(morgan("dev"));
Enter fullscreen mode Exit fullscreen mode

Logs every incoming request.

Example output:

GET /users 200 18 ms
POST /login 401 7 ms
Enter fullscreen mode Exit fullscreen mode

Very useful during development.


πŸ” Real-World Example β€” Authentication Middleware

Let's build something you'll see in almost every production Express application.

Suppose certain routes should only be accessible to logged-in users.

Instead of writing authentication logic inside every route, we move it into middleware.

const auth = (req, res, next) => {

  const token = req.headers.authorization;

  if (!token) {

    return res.status(401).json({

      message: "Unauthorized"

    });

  }

  next();

};
Enter fullscreen mode Exit fullscreen mode

Now protecting a route becomes incredibly simple.

app.get(
  "/profile",
  auth, //  auth middleware
  (req, res) => {

    res.send("Welcome to your profile!");

  }
);
Enter fullscreen mode Exit fullscreen mode

Let's see what happens when someone visits this route.

auth middleware

Without an Authorization Header

no auth token

The request stops immediately.

The route handler never executes.


With a Valid Authorization Header

with valid token

This is exactly how JWT authentication works in real-world Express applications.

We'll build one from scratch later in this series.


πŸ“š Middleware Executes in Registration Order

If you have this question in your mind

"In what order does Express execute middleware?"

The answer is simple.

Top to bottom.

Consider this example.

app.use(middlewareA);

app.use(middlewareB);

app.get("/", handler);
Enter fullscreen mode Exit fullscreen mode

A request to / produces:

middlewareA
middlewareB
handler
Enter fullscreen mode Exit fullscreen mode

Express doesn't try to optimize or reorder middleware.

It executes them exactly in the order you register them.

This is why you'll usually see middleware registered near the top of an Express application.

const express = require("express");

const app = express();

app.use(express.json());

app.use(cors());

app.use(morgan("dev"));

// Routes come afterwards...
Enter fullscreen mode Exit fullscreen mode

If you register middleware after your routes, those routes won't pass through it.

Order matters.


πŸ’₯ Error Handling Middleware

There's one special type of middleware that behaves differently.

Instead of three parameters:

(req, res, next)
Enter fullscreen mode Exit fullscreen mode

it has four.

(err, req, res, next)
Enter fullscreen mode Exit fullscreen mode

That extra err parameter tells Express:

"This middleware is responsible for handling errors."

Here's a simple example.

app.use((err, req, res, next) => {

  console.error(err);

  res.status(500).json({

    message: "Internal Server Error"

  });

});
Enter fullscreen mode Exit fullscreen mode

Whenever an error is passed to next(error) or thrown inside your application, Express skips all normal middleware and jumps directly to the nearest error-handling middleware.

This keeps your application from crashing and allows you to send a consistent error response to the client.

I'll dedicate an entire article to error handling later in this series because it's a fundamental part of building reliable production APIs.


🧠 The One Rule You'll Never Forget

If there's one thing you should remember about middleware, let it be this:

Every middleware must do exactly one of two things:

  1. Call next() to continue processing the request.

  2. Send a response to end the request.

If it does neither, Express has nowhere to go, and the request hangs forever.

Once this simple rule clicks, the entire Express middleware system becomes much easier to understand, and you'll be able to read almost any Express application with confidence.


🀯 What Broke When I First Learned Middleware

Forgot next()

Browser loading forever.

Registered middleware after routes

Middleware never executed.

Forgot express.json()

req.body was undefined.

Every Express developer has done these at least once πŸ˜„


🎯 Key Takeaways

  • Express is built around a middleware pipeline.
  • Every request follows the same lifecycle.
  • Middleware executes in registration order.
  • next() moves execution forward.
  • req contains incoming data.
  • res controls outgoing data.
  • Understanding middleware makes debugging dramatically easier.

πŸ“’ What’s Next?

In the next article we'll cover:

  • Express Router
  • Route Parameters
  • REST APIs
  • CRUD Operations
  • Project Structure
  • MVC Architecture

Once you understand Router and Middleware together, Express starts feeling ridiculously elegant πŸš€


πŸ’¬ Your Turn

What confused you most when learning Express?

  • Middleware?
  • next()?
  • req.body?
  • Route order?

Let me know in the comments πŸ‘‡


πŸ“£ Call To Action

If this article helped you:

  • ⭐ Bookmark it
  • πŸ” Share it with a fellow developer
  • πŸš€ Build a small Express API today
  • πŸ‘¨β€πŸ’» Follow for more Node.js and backend deep dives

Top comments (0)