Middleware

Qwik City comes with server middleware that allows you to centralize and chain logic such as authentication, security, caching, redirects, and logging. Middleware can also be used to define endpoints. Endpoints are useful for returning data such as RESTful API, or GraphQL API.

Middleware consists of a set of functions that are called in a specific order defined by the route. A middleware which returns a response is called an endpoint.

Middleware Function

Middleware is defined by exporting a function called onRequest (or onGet, onPost, onPut, onPatch, and onDelete) in the layout.tsx or index.tsx file inside of src/routes folder.

This example shows a simple onRequest middleware function that logs all requests.

File: src/routes/layout.tsx

import type { RequestHandler } from '@qwik.dev/city';
 
export const onRequest: RequestHandler = async ({next, url}) => {
  console.log('Before request', url);
  await next();
  console.log('After request', url);
};

If you want to intercept a specific HTTP method you can use one of these variations. If you use both onRequest and onGet for example then both will execute but onRequest will execute before onGet in the chain.

// Called only with a specific HTTP method
export const onGet: RequestHandler = async (requestEvent) => { ... }
export const onPost: RequestHandler = async (requestEvent) => { ... }
export const onPut: RequestHandler = async (requestEvent) => { ... }
export const onPatch: RequestHandler = async (requestEvent) => { ... }
export const onDelete: RequestHandler = async (requestEvent) => { ... }

Each middleware function is passed a RequestEvent object which allows the middleware to control the response.

Order of Invocation

The order of the middleware function chain is determined by their location. Starting from the topmost layout.tsx and ending at the index.tsx for a given route. (Same resolution logic as the order of layout and route component as defined by the route path.)

For example, if the request is /api/greet/ in the following folder structure, the invocation order is as follows:

src/
โ””โ”€โ”€ routes/
    โ”œโ”€โ”€ layout.tsx            # Invocation order: 1 (first)
    โ””โ”€โ”€ api/
        โ”œโ”€โ”€ layout.tsx        # Invocation order: 2   
        โ””โ”€โ”€ greet/
            โ””โ”€โ”€ index.ts      # Invocation order: 3 (last)

Qwik City looks into each file in order and checks to see if it has onRequest (or onGet, onPost, onPut, onPatch, and onDelete) exported functions. If found the function gets added into the middleware execution chain in that order.

routeLoader$ and routeAction$ are also part of the middleware. They execute after the on* functions and before the default exported component.

Component as an HTML Endpoint

You can think of component rendering as an implicit HTML endpoint. Therefore if index.tsx has a default export component, then the component implicitly becomes an endpoint in the middleware chain. Because component rendering is part of the middleware chain this allows you to intercept component rendering, for example as part of authentication, logging or other cross-cutting concerns.

import { component$ } from '@qwik.dev/core';
import { type RequestHandler } from '@qwik.dev/city';
 
export const onRequest: RequestHandler = async ({ redirect }) => {
  if (!isLoggedIn()) {
    throw redirect(308, '/login');
  }
};
 
export default component$(() => {
  return <div>You are logged in.</div>;
});
 
function isLoggedIn() {
  return true; // Mock login as true
}

RequestEvent

All middleware functions are passed a RequestEvent object which can be used to control the flow of HTTP response. For example, you can read/write cookies, headers, redirect, produce responses and exit the middleware chain early. Middleware functions are executed in the order as described above, from the topmost layout.tsx to the last index.tsx.

next()

Use the next() function to execute the next middleware function in the chain. This is the default behavior when a middleware function returns normally, without an explicit call to next(). One can use the next() function to achieve a wrapping behavior around the next middleware function.

import { type RequestHandler } from '@qwik.dev/city';
 
// Generic function `onRequest` is executed first
export const onRequest: RequestHandler = async ({ next, sharedMap, json }) => {
  const log: string[] = [];
  sharedMap.set('log', log);
 
  log.push('onRequest start');
  await next(); // Execute next middleware function (onGet)
  log.push('onRequest end');
 
  json(200, log);
};
 
// Specific functions such as `onGet` are executed next
export const onGet: RequestHandler = async ({ next, sharedMap }) => {
  const log = sharedMap.get('log') as string[];
 
  log.push('onGET start');
  // execute next middleware function
  // (in our case, there are no more middleware functions nor components.)
  await next();
  log.push('onGET end');
};

In general, a normal (non-exception) return of a function will execute the next function in the chain. However, throwing an error from the function will stop the execution chain. This is typically used for authentication or authorization and returning a 401 or 403 HTTP status code. Because the next() is implicit, it is necessary to throw an error to prevent calling the next middleware function in the chain.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onRequest: RequestHandler = async ({ next, sharedMap, json }) => {
  const log: string[] = [];
  sharedMap.set('log', log);
 
  log.push('onRequest');
  if (isLoggedIn()) {
    // normal behavior call next middleware
    await next();
  } else {
    // If not logged in throw to prevent implicit call to the next middleware.
    throw json(404, log);
  }
};
 
export const onGet: RequestHandler = async ({ sharedMap }) => {
  const log = sharedMap.get('log') as string[];
  log.push('onGET');
};
 
function isLoggedIn() {
  return false; // always return false as mock example
}

sharedMap

Use sharedMap as a way to share data between middleware functions. The sharedMap is scoped to HTTP request. A common use case is to use sharedMap to store user details so that it can be used by other middleware functions, routeLoader$() or components.

import { component$ } from '@qwik.dev/core';
import {
  routeLoader$,
  type RequestHandler,
  type Cookie,
} from '@qwik.dev/city';
 
interface User {
  username: string;
  email: string;
}
 
export const onRequest: RequestHandler = async ({
  sharedMap,
  cookie,
  send,
}) => {
  const user = loadUserFromCookie(cookie);
  if (user) {
    sharedMap.set('user', user);
  } else {
    throw send(401, 'NOT_AUTHORIZED');
  }
};
 
function loadUserFromCookie(cookie: Cookie): User | null {
  // this is where you would check cookie for user.
  if (cookie) {
    // just return mock user for this demo.
    return {
      username: `Mock User`,
      email: `mock@users.com`,
    };
  } else {
    return null;
  }
}
 
export const useUser = routeLoader$(({ sharedMap }) => {
  return sharedMap.get('user') as User;
});
 
export default component$(() => {
  const log = useUser();
  return (
    <div>
      {log.value.username} ({log.value.email})
    </div>
  );
});

headers

Use headers to set response headers associated with the current request. (For reading request headers see request.headers.) Middleware can manually add response headers to the response, using the headers property.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ headers, json }) => {
  headers.set('X-SRF-TOKEN', Math.random().toString(36).replace('0.', ''));
  const obj: Record<string, string> = {};
  headers.forEach((value, key) => (obj[key] = value));
  json(200, obj);
};

Use cookie to set and retrieve cookie information for a request. Middleware can manually read and set cookies, using the cookie function. This might be useful for setting a session cookie, such as a JWT token, or a cookie to track a user.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ cookie, json }) => {
  let count = cookie.get('Qwik.demo.count')?.number() || 0;
  count++;
  cookie.set('Qwik.demo.count', count);
  json(200, { count });
};

method

Returns current HTTP request method: GET, POST, PATCH, PUT, DELETE.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onRequest: RequestHandler = async ({ method, json }) => {
  json(200, { method });
};

url

Returns current HTTP request URL. (Use useLocation() if you need the current URL in a component. The url is meant for middleware functions.)

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ url, json }) => {
  json(200, { url: url.toString() });
};

basePathname

Returns the current base pathname URL of where the application is mounted. Typically this is / but it can be different if the application is mounted in a sub-path. See vite qwikCity({root: '/my-sub-path-location'}).

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ basePathname, json }) => {
  json(200, { basePathname });
};

params

Retrieve the "params" of the URL. For example params.myId will allow you to retrieve the myId from this route definition /base/[myId]/something.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ params, json }) => {
  json(200, { params });
};

query

Use query to retrieve the URL query parameters. (This is a shorthand for url.searchParams.) It is provided for the middleware functions, and components should use useLocation() API.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ query, json }) => {
  const obj: Record<string, string> = {};
  query.forEach((v, k) => (obj[k] = v));
  json(200, obj);
};

parseBody()

Use parseBody() to parse form data submitted to the URL.

This method will check the request headers for a Content-Type header and parse the body accordingly. It supports application/json, application/x-www-form-urlencoded, and multipart/form-data content types.

If the Content-Type header is not set, it will return null.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ html }) => {
  html(
    200,
    `
      <form id="myForm" method="POST">
        <input type="text" name="project" value="Qwik"/>
        <input type="text" name="url" value="http://qwik.dev"/>
      </form>
      <script>myForm.submit()</script>`
  );
};
 
export const onPost: RequestHandler = async ({ parseBody, json }) => {
  json(200, { body: await parseBody() });
};

cacheControl

Convenience API for setting the cache header.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({
  cacheControl,
  headers,
  json,
}) => {
  cacheControl({ maxAge: 42, public: true });
  const obj: Record<string, string> = {};
  headers.forEach((value, key) => (obj[key] = value));
  json(200, obj);
};

platform

Deployment platform (Azure, Bun, Cloudflare, Deno, Google Cloud Run, Netlify, Node.js, Vercel, etc...) specific API.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ platform, json }) => {
  json(200, Object.keys(platform));
};

locale()

Set or retrieve the current locale.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onRequest: RequestHandler = async ({ locale, request }) => {
  const acceptLanguage = request.headers.get('accept-language');
  const [languages] = acceptLanguage?.split(';') || ['?', '?'];
  const [preferredLanguage] = languages.split(',');
  locale(preferredLanguage);
};
 
export const onGet: RequestHandler = async ({ locale, json }) => {
  json(200, { locale: locale() });
};

status()

Set the status of the response independently of writing the response, useful for streaming. Endpoints can manually change the HTTP status code of the response, using the status() method.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ status, getWritableStream }) => {
  status(200);
  const stream = getWritableStream();
  const writer = stream.getWriter();
  writer.write(new TextEncoder().encode('Hello World!'));
  writer.close();
};

redirect()

Redirect to a new URL. Notice the importance of throwing to prevent other middleware functions from running. The redirect() method will automatically set the Location header to the redirect URL.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ redirect, url }) => {
  throw redirect(
    308,
    new URL('/demo/qwikcity/middleware/status/', url).toString()
  );
};

error()

Set an error response.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ error }) => {
  throw error(500, 'ERROR: Demonstration of an error response.');
};

text()

Send a text-based response. Creating a text endpoint is as simple as calling the text(status, string) method. The text() method will automatically set the Content-Type header to text/plain; charset=utf-8.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ text }) => {
  text(200, 'Text based response.');
};

html()

Send an HTML response.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ html }) => {
  html(
    200,
    ` 
      <html>
        <body>
          <h1>HTML response</h1>
        </body>
      </html>`
  );
};

json()

Creating a JSON endpoint is as simple as calling the json(status, object) method. The json() method will automatically set the Content-Type header to application/json; charset=utf-8 and JSON stringify the data.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ json }) => {
  json(200, { hello: 'world' });
};

send()

Creating a raw endpoint is as simple as calling the send(Response) method. The send() method takes a standard Response object, which can be created using the Response constructor.

import type { RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({send}) => {
  const response = new Response('Hello World', {
    status: 200,
    headers: {
      'Content-Type': 'text/plain',
    },
  });
  send(response);
};

exit()

Throw to stop the execution of the middleware functions.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ exit }) => {
  throw exit();
};

env

Retrieve environmental property in a platform-independent way.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ env, json }) => {
  json(200, {
    USER: env.get('USER'),
    MODE_ENV: env.get('MODE_ENV'),
    PATH: env.get('PATH'),
    SHELL: env.get('SHELL'),
  });
};

getWritableStream()

Set stream response.

import type { RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async (requestEvent) => {
  const writableStream = requestEvent.getWritableStream();
  const writer = writableStream.getWriter();
  const encoder = new TextEncoder();
 
  writer.write(encoder.encode('Hello World\n'));
  await wait(100);
  writer.write(encoder.encode('After 100ms\n'));
  await wait(100);
  writer.write(encoder.encode('After 200ms\n'));
  await wait(100);
  writer.write(encoder.encode('END'));
  writer.close();
};
 
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

headerSent

Check to see if the header has been set.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ headersSent, json }) => {
  if (!headersSent) {
    json(200, { response: 'default response' });
  }
};
 
export const onRequest: RequestHandler = async ({ status }) => {
  status(200);
};

request

Get the HTTP request object. Useful for getting hold of the request data such as the headers.

import { type RequestHandler } from '@qwik.dev/city';
 
export const onGet: RequestHandler = async ({ json, request }) => {
  const obj: Record<string, string> = {};
  request.headers.forEach((v, k) => (obj[k] = v));
  json(200, { headers: obj });
};

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • adamdbradley
  • manucorporat
  • mhevery
  • CoralWombat
  • EamonHeffernan
  • lollyxsrinand
  • gparlakov
  • mrhoodz
  • harishkrishnan24
  • jemsco