Skip to Content
Course content

97: Middleware in Vapor

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who was building a private API. I noticed that almost every single route handler started with the exact same five lines of code: a guard statement checking for a specific X-API-Key header, followed by a database lookup to verify that key. When he decided to change the header name to X-Auth-Token, he had to find-and-replace across forty different files. It was a tedious, error-prone mess that should have never happened.

This is exactly where Middleware comes in. Think of Middleware as a series of checkpoints or "layers" that a request must pass through before it ever hits your route logic, and that the response must pass through before it goes back to the client. Instead of polluting your controllers with repetitive logic, you pull that logic out into a standalone component that sits in the pipeline.

The Request-Response Pipeline

In Vapor, Middleware operates on a "chain." When a request hits your server, it doesn't go straight to your function; it goes into the first middleware, which can either return a response immediately (blocking the request) or pass it to the next piece of middleware in the chain. Once the final route handler generates a response, that response travels back through the middleware chain in reverse order. This allows you to modify the request on the way in and the response on the way out.

I like to visualize this as an onion. Your route handler is the core, and each piece of middleware is a layer. To get to the core, you have to peel through the layers. To leave, you have to pass back through them.

Creating a Custom API Key Guard

To build your own middleware, you implement the AsyncMiddleware protocol. The heart of this is the respond method. This is where you decide if the request is allowed to proceed. If it's not, you throw an error (like Abort(.unauthorized)) and the chain stops dead—the route handler is never even called.

import Vapor

struct APIKeyMiddleware: AsyncMiddleware {
    func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
        // 1. Inspect the request
        guard let apiKey = request.headers.first(name: "X-API-Key"), apiKey == "secret-123" else {
            throw Abort(.unauthorized, reason: "Invalid or missing API Key.")
        }
        
        // 2. Pass the request to the next middleware or the route handler
        return try await next.respond(to: request)
    }
}

Notice that next.respond(to: request) is the magic line. If you omit that, the request hangs or dies. By awaiting that call, you're saying, "I'm done with my checks; proceed to the next step." If you wanted to modify the response after the route handler finished, you would capture the result of that call in a variable, modify it, and then return it.

Global Application vs. Route Grouping

You have a choice in how you apply this. If you want every single request to your server—including your health check and documentation—to be filtered, you can register it globally in configure.swift using app.middleware.use(APIKeyMiddleware()). But usually, that's too aggressive.

More often, you'll want to protect a specific subset of routes. Vapor makes this easy with route groups. By wrapping your routes in a group that uses the middleware, you create a protected zone. Everything inside the curly braces is guarded; everything outside is public.

let protected = app.grouped(APIKeyMiddleware())

protected.get("profile") { req in
    return "This is protected data!"
}

protected.get("settings") { req in
    return "Your secret settings."
}

// This route remains public because it's not in the 'protected' group
app.get("welcome") { req in
    return "Welcome to the public API!"
}

Using groups keeps your routes.swift file clean and makes it visually obvious which parts of your API are locked down and which are open to the world.




📋 Practical Task

Building a Request Execution Timer Middleware

One of the most useful things you can do with middleware is monitor performance. Your task is to create a middleware called RequestTimerMiddleware that measures how long a request takes to be processed by the server.

Requirements:

  • Create a struct that conforms to AsyncMiddleware.
  • Capture the current time at the very start of the respond method.
  • Call try await next.respond(to: request) and store the resulting response in a constant.
  • Calculate the time elapsed between the start and the moment the response was received.
  • Print a message to the console in the format: "Request to [METHOD] [PATH] took [X]ms" (e.g., "Request to GET /profile took 12ms").
  • Register this middleware globally in your configure.swift so that every request to your server is timed.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.