Docs

Client Middleware

In Hilla, middleware intercepts the request and response for every call, so it’s able to process requests and their responses.

The middleware in Hilla is a special TypeScript async callback function that’s executed by the frontend during calls to the backend. It intercepts the request and the response for every call. The middleware has access to the call context information, including the endpoint and the method names, the supplied parameters, and other client call options.

Client middleware is an advanced topic and not recommended for most users.

When Useful

Middleware can be used to process requests and their responses. The typical use cases are:

  • performance measurement

  • logging the requests

  • retrying

  • batching

  • caching the response

  • custom request and response headers and body handling

Middleware Structure

Here is an example of logging middleware, with an explanation of the structure.

import { Middleware, MiddlewareContext, MiddlewareNext } from '@hilla/frontend';

// A middleware is an async function, that receives the `context` and `next`
export const MyLogMiddleware: Middleware = async function(
  context: MiddlewareContext,
  next: MiddlewareNext
) {
  // The context object contains the call arguments. See the `call` method
  // of the `ConnectClient` class for their descriptions.
  const {endpoint, method, params} = context;
  console.log(
    `Sending request to endpoint: ${endpoint} ` +
    `method: ${method} ` +
    `parameters: ${JSON.stringify(params)} `
  );

  // Also, the context contains the `request`, which is a Fetch API `Request`
  // instance to be sent over the network.
  const request: Request = context.request;
  console.log(`${request.method} ${request.url}`);

  // Call the `next` async function to send the request and get the response.
  const response: Response = await next(context);

  // The response is a Fetch API `Response` object.
  console.log(`Received response: ${response.status} ${response.statusText}`);
  console.log(await response?.text());

  // A middleware returns a response.
  return response;
}
Note
Request and Response are Fetch API interfaces

The Hilla middleware doesn’t invent a new data structure to represent the network request and response, but uses the interfaces declared by the Fetch API specification instead.

See the MDN web docs to learn more about the Request API and Response API.

Using Middleware with a Client

To use middleware, when the Hilla TypeScript client is instantiated, include your middleware in the middlewares array option:

import { ConnectClient } from '@hilla/frontend';
import { MyLogMiddleware } from './my-log-middleware';

const client = new ConnectClient({
  prefix: '/connect',
  middlewares: [MyLogMiddleware]
});

export default client;

Alternatively, you can modify the middlewares array property on the existing client, for example if you use a generated client:

import client from 'Frontend/generated/connect-client.default';
import { MyLogMiddleware } from './my-log-middleware';

client.middlewares = [MyLogMiddleware];
Caution
Modifying middleware at runtime
If you modify the middlewares array, only calls initiated after the modification use the new middlewares array. To avoid issues connected with this, it’s better to avoid modifying middlewares, or to modify middlewares only before the first call.

Changing a Request Using Middleware

To make a low-level modification of the request in middleware, replace the context.request with a new Fetch API Request instance:

import { Middleware, MiddlewareContext, MiddlewareNext } from '@hilla/frontend';

// An example middleware that uses a different server for selected requests
export const MyApiDispatcherMiddleware: Middleware = async function(
  context: MiddlewareContext,
  next: MiddlewareNext
) {
  if (context.endpoint === 'ExternalEndpoint') {
    const url = context.request.url.replace(
      'https//my-app.example.com',
      'https://external-endpoint.example.com'
    );
    context.request = new Request(url, context.request);
  }

  return await next(context);
};

Custom Response using Middleware

Middleware can also replace the response by returning a custom Response instance:

import { Middleware, MiddlewareContext, MiddlewareNext } from '@hilla/frontend';

// An example middleware that returns an empty response instead of calling the backend endpoint
export const MyStubMiddleware: Middleware = async function(
  context: MiddlewareContext,
  next: MiddlewareNext
) {
  if (context.endpoint === 'StubEndpoint') {
    //
    return new Response('{}');
  }

  return await next(context);
}