blomqma / next-rest-framework

Type-safe, self-documenting APIs for Next.js
https://next-rest-framework.vercel.app
Other
157 stars 20 forks source link

How to implement middleware for all `GET` requests #38

Closed FelixZY closed 1 year ago

FelixZY commented 1 year ago

I would like to add global cache headers to all GET requests. How can I use middleware to do this?

In a more general sense: I'm missing some practical examples of how to implement the different types of middleware (global/route/method).

FelixZY commented 1 year ago

Hm, I think I got it by defining my middleware in [[...next_rest_framework]].ts:

export default defineCatchAllHandler({
  middleware({ req, res }) {
    // Cache requests
    // https://vercel.com/docs/concepts/functions/serverless-functions/edge-caching#stale-while-revalidate
    if (req.method === "GET") {
      res.setHeader("Cache-Control", "s-maxage=3600, stale-while-revalidate");
    }
    return {};
  },
});

However, I have so far been unsuccessful in splitting the middleware into a separate file due to typescript complaints. What type should I use when moving middleware to a separate function? I'm guessing something like

export function authenticated(): Middleware<{}> {
  return async ({ req, res }) => {
    const token = await getToken({ req });
    if (!token) {
      res.status(401).end();
    }
    return {};
  }
}

but have so far not gotten this to work:

Argument of type '{ GET: { output: { status: number; contentType: "application/json"; schema: ZodVoid; }[]; middleware: Middleware<{}>; handler({ res }: { req: TypedNextApiRequest<yup.InferType<T>, yup.InferType<T>>; res: TypedNextApiResponse<...>; params: Record<...> | Record<...>; }): Promise<...>; }; }' is not assignable to parameter of type 'DefineEndpointsParams<unknown, unknown, { status: number; contentType: "application/json"; schema: ZodVoid; }, {}, unknown, unknown, OutputObject<number, AnyContentTypeWithAutocompleteForMostCommonOnes, any>, ... 26 more ..., unknown>'.
  The types of '[ValidMethod.GET].middleware' are incompatible between these types.
    Type 'Middleware<{}>' is not assignable to type 'Middleware<{}, { params: Record<string, unknown>; }, TypedNextApiRequest<yup.InferType<T>, yup.InferType<T>>, TypedNextApiResponse<number, "application/json", void>>'.
      Type 'TypedNextApiResponse<number, "application/json", void>' is not assignable to type 'NextApiResponse'.
        Type 'TypedNextApiResponse<number, "application/json", void>' is not assignable to type 'ServerResponse<IncomingMessage>'.
          The types returned by 'setHeader(...)' are incompatible between these types.
            Type 'void' is not assignable to type 'ServerResponse<IncomingMessage>'.
blomqma commented 1 year ago

Hey, yeah using the global middleware is one way to set the cache headers for all GET requests. Another way would be to do it using Next.js' native Middleware: https://nextjs.org/docs/pages/building-your-application/routing/middleware

About your TypeScript issue, I can see that your middleware function is a function that returns an async function. With your approach it only works if you actually call it when defining the middleware in your API handler to get the correct middleware function. If you don't want to call the function, here's a correct way to do it:

const authenticated: Middleware<{}> = async ({ req, res }) => {
    const token = await getToken();
    if (!token) {
      res.status(401).end();
    }
    return {};
}

And then you can use it like this:

export default defineCatchAllHandler({
  middleware: authenticated
});
FelixZY commented 1 year ago

@blomqma Hm, that does not seem to work for me:

const authenticated: Middleware<{}> = async ({ req, res }) => {
  const token = await getToken({ req });
  if (!token) {
    res.status(401).end();
  }
  return {};
};

export default defineEndpoints({
  GET: {
    output: [
      {
        status: 204,
        contentType: "application/json",
        schema: z.void(),
      },
      {
        status: 401,
        contentType: "application/json",
        schema: z.void(),
      },
    ],
    openApiSpecOverrides: {
      summary: "Seed development database",
      security: [
        {
          DeveloperAuth: [],
        },
      ],
    } as Partial<OpenAPIObject["paths"][string]["get"]>,
    middleware: authenticated,
    async handler({ res }) {
      await seedDev();

      res.setHeader("content-type", "application/json");
      res.status(204).json();
    },
  },
});

gives the same error

Argument of type '{ GET: { output: { status: number; contentType: "application/json"; schema: ZodVoid; }[]; openApiSpecOverrides: Partial<OperationObject | undefined>; middleware: Middleware<...>; handler({ res }: { ...; }): Promise<...>; }; }' is not assignable to parameter of type 'DefineEndpointsParams<unknown, unknown, { status: number; contentType: "application/json"; schema: ZodVoid; }, {}, unknown, unknown, OutputObject<number, AnyContentTypeWithAutocompleteForMostCommonOnes, any>, ... 26 more ..., unknown>'.
  The types of '[ValidMethod.GET].middleware' are incompatible between these types.
    Type 'Middleware<{}>' is not assignable to type 'Middleware<{}, { params: Record<string, unknown>; }, TypedNextApiRequest<yup.InferType<T>, yup.InferType<T>>, TypedNextApiResponse<number, "application/json", void>>'.
      Type 'TypedNextApiResponse<number, "application/json", void>' is not assignable to type 'NextApiResponse'.
        Type 'TypedNextApiResponse<number, "application/json", void>' is not assignable to type 'ServerResponse<IncomingMessage>'.
          The types returned by 'setHeader(...)' are incompatible between these types.
            Type 'void' is not assignable to type 'ServerResponse<IncomingMessage>'.

Sidenote: it would be nice with some built-in typings for the openApiSpecOverrides. I'm using openapi3-ts for this currently.