Phestus v0.1 is just weeks away from being released! Explore the latest modules and documentation.Learn more

Middleware Creation

Middleware Creation

Creating middleware in Phestus requires implementing the Middleware interface and registering the resulting operation with the Middleware Module.

A middleware operation can perform logic before an endpoint runs, after an endpoint runs, or prevent the endpoint from running entirely.

1. Create the Middleware

Start by importing the Middleware type:

import type { Middleware } from "@phestus/middleware-module";

A middleware operation must provide a name and a handle() method.

For example, create a simple request logging middleware:

const loggingMiddleware: Middleware = {
    name: "logging",

    async handle(request, next) {
        console.log(`${request.method} ${request.path}`);

        return next();
    },
};

The name identifies the middleware and is used by API endpoints when selecting middleware.

The handle() method receives the incoming request and the next() function.

2. Inspect the Request

The MiddlewareRequest provides access to common request information:

interface MiddlewareRequest {
    method: string;
    path: string;
    body: unknown;
    params: Record<string, string>;
    query: Record<string, string>;
    headers: Record<string, string>;
    context: Record<string, unknown>;
}

For example, middleware can inspect request headers:

const loggingMiddleware: Middleware = {
    name: "logging",

    async handle(request, next) {
        console.log(request.headers);

        return next();
    },
};

It can also inspect the request method, path, parameters, query, body, or context.

3. Continue Execution

A middleware operation continues execution by calling next():

const loggingMiddleware: Middleware = {
    name: "logging",

    async handle(request, next) {
        console.log("Request started");

        const response = await next();

        console.log("Request completed");

        return response;
    },
};

The important detail here is that next() returns the response generated by the remaining middleware and eventually the endpoint handler.

This means middleware can execute logic both before and after the endpoint:

Middleware
    │
    ├── Before
    │
    ▼
  next()
    │
    ▼
Next Middleware
    │
    ▼
Endpoint Handler
    │
    ▲
    │
Response
    │
    ├── After
    │
    ▼
Middleware Response

4. Stop Execution

A middleware operation can prevent the request from reaching the endpoint by returning its own response.

For example:

const maintenanceMiddleware: Middleware = {
    name: "maintenance",

    async handle(request, next) {
        const maintenance = true;

        if (maintenance) {
            return {
                status: 503,
                data: {
                    message: "Service unavailable",
                },
            };
        }

        return next();
    },
};

Because next() is not called, execution stops at this middleware.

This pattern can be used for access controls, validation, rate limiting, maintenance modes, or any other condition that should prevent an endpoint from executing.

5. Register the Middleware

Once the middleware operation has been created, register it with the Middleware Module:

// Custom method to return phestus object
const phestus = getPhestus();

const middleware = phestus.getModule("middleware");

middleware.use(loggingMiddleware);
middleware.use(maintenanceMiddleware);

The use() method adds the middleware operation to the module's registry.

You can register as many middleware operations as your application requires:

middleware.use(authenticationMiddleware);
middleware.use(authorizationMiddleware);
middleware.use(validationMiddleware);
middleware.use(loggingMiddleware);

The Middleware Module maintains the registered operations internally.

6. Attach Middleware to an API Endpoint

API endpoints select middleware by name:

const endpoint: ApiEndpoint = {
    method: "GET",
    path: "/users",

    middleware: [
        "authentication",
        "authorization",
    ],

    async handler(request) {
        return {
            status: 200,
            data: [],
        };
    },
};

The names in the middleware array correspond to the names assigned to registered middleware operations.

For example:

const authenticationMiddleware: Middleware = {
    name: "authentication",

    async handle(request, next) {
        // Authentication logic

        return next();
    },
};

The endpoint can reference it using:

middleware: ["authentication"]

7. Execute the Middleware

The Middleware Module exposes execute() for running middleware:

await middleware.execute(
    request,
    handler,
    ["authentication", "authorization"],
);

The first argument is the middleware request.

The second argument is the final handler that should execute after all selected middleware has completed.

The third argument optionally specifies which middleware operations should run.

Conceptually, the execution becomes:

Request
   │
   ▼
authentication
   │
   ▼
authorization
   │
   ▼
handler
   │
   ▼
Response

If no middleware names are supplied, the module uses all registered middleware:

await middleware.execute(
    request,
    handler,
);

Complete Example

A complete middleware operation can therefore be very small.

import type { Middleware } from "@phestus/middleware-module";

export const authenticationMiddleware: Middleware = {
    name: "authentication",

    async handle(request, next) {
        const token = request.headers.authorization;

        if (!token) {
            return {
                status: 401,
                data: {
                    message: "Authentication required",
                },
            };
        }

        return next();
    },
};

Register it with the Middleware Module:

middleware.use(authenticationMiddleware);

Then reference it from an API endpoint:

const endpoint: ApiEndpoint = {
    method: "GET",
    path: "/profile",

    middleware: [
        "authentication",
    ],

    async handler(request) {
        return {
            status: 200,
            data: {
                message: "Authenticated request",
            },
        };
    },
};

The resulting request flow is:

GET /profile
      │
      ▼
Authentication Middleware
      │
      ├── No token ──► 401 Response
      │
      ▼
   next()
      │
      ▼
Endpoint Handler
      │
      ▼
   200 Response

Creating More Advanced Middleware

Middleware does not need to be limited to a single check.

Because a middleware operation controls whether next() is called, it can implement arbitrary application-specific logic.

For example:

const authorizationMiddleware: Middleware = {
    name: "authorization",

    async handle(request, next) {
        const actor = request.context.actor;

        if (!actor) {
            return {
                status: 401,
                data: {
                    message: "Authentication required",
                },
            };
        }

        const allowed = true;

        if (!allowed) {
            return {
                status: 403,
                data: {
                    message: "Forbidden",
                },
            };
        }

        return next();
    },
};

The application can decide how that logic is implemented. The Middleware Module only provides the execution mechanism.

This allows middleware to remain a general-purpose extension point while modules such as Auth provide specialized capabilities that middleware can consume.

Summary

A middleware operation follows a simple lifecycle:

Create
  │
  ▼
Register
  │
  ▼
Attach to Endpoint
  │
  ▼
Execute
  │
  ├── Return response
  │
  └── Call next()
          │
          ▼
      Next operation
          │
          ▼
      Endpoint handler

The Middleware Module provides the execution layer, while individual middleware operations define the application's behavior.

This makes it possible to create custom request-processing rules without adding those rules directly to the API or Auth modules.

These two should fit nicely as Concepts → Middleware → Introduction and Concepts → Middleware → Middleware Creation, with the first explaining the architecture and the second being the practical implementation guide.