Middleware

In v2.x, we use the term "services" to encapsulate any software used in a Drash application that is not part of Drash's core code. We are changing that definition for v3.x onward. We now use the MDN's definition to describe middleware.

Summary

Middleware is software that can run before and after a resource's HTTP method is called. Middleware can be added to resources using a resource group.

The highlighted sections below show how middleware can be added:

// Code is shortened for brevity
 
import { Chain, Resource } from "https://esm.sh/@drashland/drash@v3.0.0-beta.1/modules/chains/RequestChain/mod.native.ts";
 
const groupedResources = Resource
  .group()            // Get the class' builder for quickly building a group
  .resources(         // Add these resources
    ResourceA,
    ResourceB,
    ResourceC,
  )
  .middleware(        // Add these middleware classes to each resource
    SomeMiddlewareA,  // During runtime, SomeMiddlewareA will run first, then ...
    SomeMiddlewareB,  // ... SomeMiddlewareB will run, then the resource will be called
  )
  .build();           // Build the group
 
// Create the chain and add the resource group to it
const chain = Chain
  .builder()
  .resources(groupedResources)
  .build();

Creating Middleware

Handling Requests

You can create middleware by extending the Request Chain module's Middleware class.

Below is an example of creating a logging middleware that logs GET requests to a resource.

import { Chain, Middleware, Resource } from "https://esm.sh/@drashland/drash@v3.0.0-beta.1/modules/chains/RequestChain/mod.native.ts";
 
// Create the resource
class MyResource extends Resource {
    public paths = ["/"];
 
    public GET(request) {
        return new Response("Hello!");
    }
}
 
// Create the middleware
class LogGETRequests extends Middleware {
  public GET(request) {
      // Log the request
      //
      // A logging library is a better option, but for educational purposes,
      // a `console.log()` statement will suffice.
      console.log(`Incoming request: ${request.method} ${request.url}`);
 
      // Call the resource's GET method. `super` will be `MyResource` when
      // they are built in their resource group.
      return super.GET(request);
  }
}
 
const group = Resource
  .group()
  .resources(MyResource)      // Add the resource to the group
 
  .middleware(LogGETRequests) // Add the middleware to the group. During the
                              // resource group's build process, this
                              // middleware will end up EXTENDING the
                              // `MyResource` class -- hence the
                              // `super.GET()` call in the resource. `super`
                              // will be `MyResource`, so `super.GET()` will
                              // call `MyResource.GET()`.
  .build();
 
const chain = Chain
  .builder()
  .resources(group)          // Chains can take in resources and resource groups
  .build();

You might notice that the LogGETRequests middleware class has a GET method defined (just like a resource). The Request Chain module's Middleware class extends the same core Resource class that the Request Chain module's Resource class extends. This means all middleware classes you create will inherit the same HTTP methods that the Request Chain module's Resource class inherits. This also means the following:

  • If you want your middleware to handle GET requests, then add a GET method to your middleware
  • If you want your middleware to handle POST requests, then add a POST method to your middleware
  • If you want your middleware to handle PUT requests, then add a PUT method to your middleware
  • ... and so on

Handling All Requests

Middleware classes also have an ALL method. This method allows your middleware to run on all requests to your resource(s) — hence the method name being ALL. Taking the above LogGETRequests class, making it log on all requests looks like:

import { Chain, Middleware, Resource } from "https://esm.sh/@drashland/drash@v3.0.0-beta.1/modules/chains/RequestChain/mod.native.ts";
 
// Create the resource
class MyResource extends Resource {
    public paths = ["/"];
 
    public GET(request) {
        return new Response("Hello!");
    }
}
 
// Create the middleware
class LogGETRequests extends Middleware {
  public ALL(request) {
      // Log the request
      //
      // A logging library is a better option, but for educational purposes,
      // a `console.log()` statement will suffice.
      console.log(`Incoming request: ${request.method} ${request.url}`);
 
      // Call the resource's GET method. `super` will be `MyResource` when
      // they are built in their resource group.
      return super.GET(request);
  }
}
 
const group = Resource
  .group()
  .resources(MyResource)      // Add the resource to the group
 
  .middleware(LogGETRequests) // Add the middleware to the group. During the
                              // resource group's build process, this
                              // middleware will end up EXTENDING the
                              // `MyResource` class -- hence the
                              // `super.GET()` call in the resource. `super`
                              // will be `MyResource`, so `super.GET()` will
                              // call `MyResource.GET()`.
  .build();
 
const chain = Chain
  .builder()
  .resources(group)          // Chains can take in resources and resource groups
  .build();

The above code will result in LogGETRequests logging every request made to MyResource.

Sorting Middleware

Middleware runs in the order you define them (from top-to-bottom or left-to-right depending on how you write your code). For example:

// Code is shortened for brevity
 
const groupedResources = Resource
  .group()
  .resources(
    ResourceA,
    ResourceB,
    ResourceC,
  )
  .middleware(
    SomeMiddlewareA, // This runs first
    SomeMiddlewareB, // This runs second
    SomeMiddlewareC, // This runs third
  )
  .build();

How It Works

TLDR

When a resource group is built with resources and middleware, the build process takes all middleware and places them around the resources. This allows middleware to run processes before and after a resource's HTTP method is called.

Detailed Explanation

When a resource group is built with resources and middleware, the build process uses a variant of the Proxy Pattern to place middleware around the resources. This allows middleware to run processes before and after a resource's HTTP method is called like below.

Example

This example shows how middleware can:

  • block inbound requests to a resource if the x-auth-token header is bad;
  • send requests to a resource; and
  • ensure the response from the resource is tagged with a x-auth-by header.
class AuthMiddleware extends Middleware {
  public ALL(request: Request) {
 
    const token = request.headers.get("x-auth-token");
 
    // No token or not verified? Get out.
    if (!token || !SomeVerificationService.verify(token)) {
      throw new HTTPError(401, `Denied`);
    }
 
    // Otherwise, do your thing man. Proceed.
    const response = super.GET(request);
 
    // Alright, before we send the response, let's make sure we tag the response
    // with some identifier that can be used by outbound/upstream processes.
    response.headers.set("x-auth-by", "Drash-Auth-v2.3");
 
    // Cool. Now we can send the response knowing outbound/upstream processes
    // can identify the request/response as being authorized by Drash-Auth-v2.3.
    return response;
  }
}

Data Flow

This example shows how data flows from middleware classes to resource classes. The middleware classes below (in the .middleware() call) will be placed around ResourceA, ResourceB, and ResourceC. When requests are made to any of the resources, the requests must go through each middleware before being passed to the resource they are targeting.

// Code is shortened for brevity
 
import { Chain, Resource } from "https://esm.sh/@drashland/drash@v3.0.0-beta.1/modules/chains/RequestChain/mod.native.ts";
 
const groupedResources = Resource
  .group()            // Get the class' builder for quickly building a group
  .resources(         // Add these resources
    ResourceA,
    ResourceB,
    ResourceC,
  )
  .middleware(        // Add these middleware classes to each resource
    SomeMiddlewareA,  // During runtime, SomeMiddlewareA will run first, then ...
    SomeMiddlewareB,  // ... SomeMiddlewareB will run, then the resource will be called
  )
  .build();           // Build the group
 
// Create the chain and add the resource group to it
const chain = Chain
  .builder()
  .resources(groupedResources)
  .build();

Taking the above code, the request flows for each resource will be:

As you can see above, requests must go through all middleware before being passed to the resources.