CodingNic

Middleware

Custom Middleware

Middleware 12 min read

Custom Middleware

Objectives

By the end of this lesson, you should be able to:

  • Write a custom middleware function that inspects or rejects a request
  • Apply middleware to a single route instead of the whole app
  • Decide between calling next() and sending a response directly

💡 Why this matters: Built-in and third-party middleware (Lessons 2, 4-8) cover common needs, but application-specific logic, checking an API key, validating something about a request, is usually a custom middleware function written for exactly one project.

⚠️ A note on verification: every snippet and every response shown below was actually run and tested with real HTTP requests.

A Middleware That Can Reject a Request

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

function requireApiKey(req, res, next) {
  const key = req.get('X-API-Key');
  if (key !== 'secret123') {
    return res.status(401).json({ error: 'Invalid or missing API key' });
  }
  next();
}

app.get('/public', (req, res) => {
  res.json({ message: 'anyone can see this' });
});

app.get('/private', requireApiKey, (req, res) => {
  res.json({ message: 'only with a valid key' });
});

app.listen(4202);
bash
curl http://localhost:4202/public
text
{"message":"anyone can see this"}
bash
curl -w " [%{http_code}]" http://localhost:4202/private
text
{"error":"Invalid or missing API key"} [401]
bash
curl -w " [%{http_code}]" -H "X-API-Key: secret123" http://localhost:4202/private
text
{"message":"only with a valid key"} [200]

requireApiKey either sends a 401 response directly (and doesn’t call next(), since the request should stop here, never reaching the route handler) or calls next() to let a valid request continue. The return before res.status(401).json(...) matters (Module 5), without it, execution would fall through and call next() anyway, letting an invalid request through.

Applying Middleware to a Single Route

app.get('/private', requireApiKey, (req, res) => {...}) passes requireApiKey as a second argument, between the path and the final handler, Express runs it only for requests to /private, not for /public or any other route. This is different from app.use(requireApiKey), which would apply it to every route in the app. A route can even take multiple middleware functions this way, app.get(path, middleware1, middleware2, handler), each calling next() in turn before the final handler runs.

When to Call next() vs Send a Response

A middleware function does exactly one of two things: call next() to continue the pipeline (an authentication check that passed, a logger that just records something), or send a response and stop (an authentication check that failed, a validation error). Never do both, calling next() after already sending a response causes a real Express error (Cannot set headers after they are sent), since the request would be handled twice.

Try It

  1. Write a custom middleware requireApiKey (or similar) and apply it to a single route using the app.get(path, middleware, handler) pattern, confirming other routes are unaffected.
  2. Write a second custom middleware that validates something about req.body (for example, checking a required field exists) on a POST route, sending a 400 if it’s missing.
  3. Deliberately call both next() and res.send() in the same middleware, run it, and observe the resulting Express error, then fix it by choosing one or the other.
  4. Explain, in your own words, the difference between app.use(middleware) and app.get(path, middleware, handler).

Recap

  • Custom middleware inspects a request, and either calls next() to continue, or sends a response and stops, never both.
  • Passing middleware as an extra argument to app.get/post/etc. applies it only to that specific route, not the whole app.
  • Custom middleware is where application-specific request-handling logic (authentication checks, validation) typically lives.

Next lesson: Morgan, logging every incoming request automatically.