Server
Middleware

Middleware

The Middleware function is executed before each route call, whether it's a query or mutation, that utilizes the resolver associated with the middleware. The next function within the middleware determines when to proceed to the next middleware in the chain. This mechanism enables you to execute custom logic, perform validation, or modify the context before the resolver is invoked, ensuring consistent behavior and enforcing any necessary preconditions across your application's routes.

import { ptsq } from '@ptsq/server';
import express, { Request, Response } from 'express';
import { getUserFromJWT } from './getUserFromJWT';
import { userRouter } from './userRouter';
 
const app = express();
 
const createContext = async ({ req, res }: { req: Request; res: Response }) => {
  const user = await getUserFromJWT(req.cookies.jwt);
 
  return {
    req,
    res,
    user,
  };
};
 
const { resolver, router } = ptsq({
  ctx: createContext,
}).create();
 
const protectedResolver = resolver.use(({ ctx, next }) => {
  if (!ctx.user) throw new Error('Must be logged in!');
 
  return next({
    ...ctx,
    user: ctx.user,
  });
});
 
resolver.query({
  resolve: async ({
    ctx /* { user: User | null } */,
    input /* undefined */,
  }) => {
    return `Hello, ${ctx.user.name}`;
    // throws Error user can be null
  },
});
 
protectedResolver.query({
  resolve: async ({ ctx /* { user: User } */, input /* undefined */ }) => {
    return `Hello, ${ctx.user.name}`;
  },
});

The next function only returns the passed context. It is only for keeping the context type up to date after e.g. if condition.

Run next function before return

Running the next function before the return statement allows for the execution of subsequent middlewares before the current middleware completes. This capability is particularly useful for tasks such as measuring response times, as it ensures that the next middleware is triggered before the current one finishes processing.

Resolve function always runs as the final middleware in the chain. This ensures that the resolver logic is executed last, allowing for any necessary post-processing or finalization steps before sending the response back to the client.

const measuredResolver = resolver.use(({ ctx, next }) => {
  if (!ctx.user) throw new Error('Must be logged in!');
 
  const timeStart = performance.now();
 
  const resolverResult = next({
    ...ctx,
    user: ctx.user,
  });
 
  const time = performance.now() - timeStart;
 
  console.log('Time to resolve: ', time);
  console.log('The result of resolver is: ', resolverResult);
 
  return resolverResult;
});

Indeed, returning the resolver result is essential not only for inferring the context type but also for passing the result up the middleware chain. This approach enables the nesting of multiple middlewares, with each subsequent middleware potentially modifying the result before passing it further up the chain. This recursive passing of results ensures that each middleware has the opportunity to intercept and process the data, facilitating a flexible and extensible middleware architecture within your application.

The type of middleware response is

type MiddlewareResponse =
  | {
      ok: true;
      data: unknown;
    }
  | {
      ok: false;
      error: PtsqError;
    };

so you have to determine if the ok property is true or false to access the data or error.

Arguments validation with middleware

You can validate part of arguments before the middleware definition, so you can access the properties in the middleware.

const protectedResolver = resolver
  .args(
    Type.Object({
      firstName: Type.String(),
    }),
  )
  .use(
    ({
      ctx /* { user: User<any> | null } */,
      input /* { firstName: string } */,
      next,
    }) => {
      if (!ctx.user || ctx.user.firstName !== input.firstName)
        throw new Error('Must be logged in with firstName match!');
 
      return next({
        ctx: {
          user: ctx.user,
        },
      });
    },
  );