Middleware

Middleware is a function that intervenes in the processing flow of a parsed function. It provides a way to insert logic into the request and response flow to execute code before a response is sent or before a request is processed. GQLoom's middleware follows the onion middleware pattern of Koa.

Defining middleware

A middleware is a function that takes two arguments: next and payload.

  • next is a function that represents the next middleware. When next is called, the next middleware will be executed.

  • payload is an object that contains information about the field currently being parsed, specifically the following fields:

    • outputSilk: output silk containing the output type of the field currently being parsed;
    • parent: the parent of the current field, equivalent to useResolverPayload().root;
    • parseInput: the function used to get the input of the current field;
    • type: the type of the current field, with a value of query, mutation, subscription or field;

Additionally, we can get the context of the current parser function and more with useContext() and useResolverPayload().

A minimal middleware function is as follows:

import { Middleware } from '@gqloom/core'; const middleware: Middleware = async (next) => { return await next(); }

Next, we'll introduce some common types of middleware.

Error catching

When using Valibot or Zod libraries for input validation, we can catch validation errors in the middleware and return customized error messages.

valibot
zod
import { type Middleware } from "@gqloom/core" import { ValiError } from "valibot" import { GraphQLError } from "graphql" export const ValibotExceptionFilter: Middleware = async (next) => { try { return await next() } catch (error) { if (error instanceof ValiError) { const { issues, message } = error throw new GraphQLError(message, { extensions: { issues } }) } throw error } }

Validate output

In GQLoom, validation of parser output is not performed by default. However, we can validate the output of parser functions through middleware.

import { silk, type Middleware } from "@gqloom/core" export const outputValidator: Middleware = async (next, { outputSilk }) => { const output = await next() return await silk.parse(outputSilk, output) }

Let's try to use this middleware:

valibot
zod
import { weave, resolver, query } from "@gqloom/core" import { ValibotWeaver } from "@gqloom/valibot" import * as v from "valibot" import { createServer } from "node:http" import { createYoga } from "graphql-yoga" import { outputValidator, ValibotExceptionFilter } from "./middlewares" const HelloResolver = resolver({ hello: query(v.pipe(v.string(), v.minLength(10)), { input: { name: v.string() }, resolve: ({ name }) => `Hello, ${name}`, middlewares: [outputValidator], }), }) export const schema = weave(ValibotWeaver, HelloResolver, ValibotExceptionFilter) const yoga = createYoga({ schema }) createServer(yoga).listen(4000, () => { // eslint-disable-next-line no-console console.info("Server is running on http://localhost:4000/graphql") })

In the code above, we added the v.minLength(10) requirement to the output of the hello query and added the outputValidator middleware to the parser function. We also added a global middleware ValibotExceptionFilter to weave.

When we make the following query:

{ hello(name: "W") }

A result similar to the following will be given:

valibot
zod
{ "errors": [ { "message": "Invalid length: Expected >=10 but received 8", "locations": [ { "line": 2, "column": 3 } ], "path": [ "hello" ], "extensions": { "issues": [ { "kind": "validation", "type": "min_length", "input": "Hello, W", "expected": ">=10", "received": "8", "message": "Invalid length: Expected >=10 but received 8", "requirement": 10 } ] } } ], "data": null }

If we adjust the input so that the returned string is the required length:

{ hello(name: "World") }

It will get a response with no exceptions:

{ "data": { "hello": "Hello, World" } }

Authentication

Checking a user's permissions is a common requirement that we can easily implement with middleware.

Consider that our user has the roles “admin” and “editor”, and we want the administrator and editor to have access to their own actions, respectively. First, we implement an authGuard middleware that checks the user's role:

import { type Middleware } from "@gqloom/core" import { useUser } from "../context" import { GraphQLError } from "graphql" export function authGuard(role: "admin" | "editor"): Middleware { return async (next) => { const user = await useUser() if (user == null) throw new GraphQLError("Not authenticated") if (!user.roles.includes(role)) throw new GraphQLError("Not authorized") return next() } }

In the code above, we declare an authGuard middleware that takes a role parameter and returns a middleware function. The middleware function checks that the user is authenticated and has the specified role, and throws a GraphQLError exception if the requirements are not satisfied.

We can apply different middleware for different resolvers:

valibot
zod
import { resolver, mutation } from "@gqloom/core" import * as v from "valibot" import { authGuard } from "./middlewares" const AdminResolver = resolver( { deleteArticle: mutation(v.boolean(), () => true), }, { middlewares: [authGuard("admin")], } ) const EditorResolver = resolver( { createArticle: mutation(v.boolean(), () => true), updateArticle: mutation(v.boolean(), () => true), }, { middlewares: [authGuard("editor")] } )

In the code above, we have applied the authGuard middleware to AdminResolver and EditorResolver and assigned different roles to them. In this way, only users with the corresponding roles can access the actions within the corresponding resolvers.

Logging

We can also implement logging functionality through middleware. For example, we can create a logger middleware to log the execution time of each field parsing function:

import { type Middleware, useResolverPayload } from "@gqloom/core" export const logger: Middleware = async (next) => { const info = useResolverPayload()!.info const start = Date.now() const result = await next() const resolveTime = Date.now() - start console.log(`${info.parentType.name}.${info.fieldName} [${resolveTime} ms]`) return result }

Using middleware

GQLoom is able to apply middleware in a variety of scopes, including resolver functions, resolver local middleware, and global middleware.

Resolve function middleware

We can use middleware directly in the resolve function by simply passing the middlewares field in the second argument of the operation constructor, for example:

valibot
zod
import { resolver, query } from "@gqloom/core" import * as v from "valibot" import { outputValidator } from "./middlewares" const HelloResolver = resolver({ hello: query(v.pipe(v.string(), v.minLength(10)), { input: { name: v.string() }, resolve: ({ name }) => `Hello, ${name}`, middlewares: [outputValidator], }), })

Resolver-scoped middleware

We can also apply middleware at the resolver level, which will take effect for all operations within the resolver. Simply pass the middlewares field in the last argument of the resolver constructor, for example:

valibot
zod
import { resolver, mutation } from "@gqloom/core" import * as v from "valibot" import { authGuard } from "./middlewares" const AdminResolver = resolver( { deleteArticle: mutation(v.boolean(), () => true), }, { middlewares: [authGuard("admin")], } ) const EditorResolver = resolver( { createArticle: mutation(v.boolean(), () => true), updateArticle: mutation(v.boolean(), () => true), }, { middlewares: [authGuard("editor")] } )

Global middleware

In order to apply global middleware, we need to pass in the middleware fields in the weave function, for example:

import { weave } from "@gqloom/core" import { ExceptionFilter } from "./middlewares" export const schema = weave(HelloResolver, ExceptionFilter)