4

I'm using next-connect with next.js & typescript, and I want to create a middleware that adds some fields to request object and infer the new request type. The code below:

// multipart middleware
export type NextApiRequestMultipart = NextApiRequest & {
  files: Files;
  fields: Fields;
};
export function multipart(
  config?: Options
) {
  return async (
    req: NextApiRequest,
    res: NextApiResponse,
    next: NextHandler
  ) => {
    const { files, fields } = await parseForm(req, config);
    (req as NextApiRequestMultipart).files = files;
    (req as NextApiRequestMultipart).fields = fields;
    return next();
  };
}
export router().post(
  multipart({ multiples: false }),
  async (req, res) => {
    // I want to access properties without statically typing the request
    const { files, fields } = req;
  }
);

StackBlitz repo: see code

Ismail_Aj
  • 332
  • 1
  • 4
  • 17

2 Answers2

3

I managed to implement as sample solution. Here is the code demo: stakc-blitz modified

Sample description of the approach

I have not tested it but I wanted to show the approach.

We need a controller router builder to do that. And this controller builder will need to "stack" the types additions to the Request object of all middleware.

A sample


class ControllerBuilder<RequestType> {
   addMiddleWare(middleWare): ControllerBuilder<RequestType & middlewareTypeAdditions> {
   // implementation
 }
}

In order to extract the middleware type - I need to have it stated someware. That is why I introduced a decorated middleware.

Here is the abstraction over the Decorate middleware:


abstract class DecoratedMiddleware<MiddlewareReqTypeAdditions> {
  ///
}

Now in the ControllerBuilder we can "extract the type" of each middleware and "stack" them by returning new instance with unin type: ReqeustType so far united with the addition the new middleware will add

class ControllerBuilder<RequestType> {
   addMiddleWare(middleWare: DecoratedMiddleware<MiddlewareReqTypeAdditions>): ControllerBuilder<RequestType & MiddlewareReqTypeAdditions> {
   // implementation
   return new ControllerBuilder<>
 }
}

Here is a sample middleware implementation of. We only need to state the additional properties of the request, which the builder will set. The process function has to return a Promise of those props, ensuring that all is set according to the middleware type contract.

type AuthRequestAddtion = {
  role: string;
  id: number | string;
  hotelId: number;
};

class AuthMiddleware extends DecoratedMiddleware<AuthRequestAddtion> {
  protected process: MuddlewareFunc<AuthRequestAddtion> = (req, res) => {
    return Promise.resolve({
      id: 1,
      role: 'GUEST',
      hotelId: 3,
    });
  };
}

And finally a sample usage:


ControllerBuilder.get(router(), '/with-weather')
  .addMiddleware(authMiddleware)
  .addMiddleware(multipartMiddleware)
  .addMiddleware(weatherMiddleware)
  .handle(async (req, res) => {
    //now we have types for all the middlewares
    const hotelId = req.hotelId;
    const files = req.files;
    const temp = req.weather.temperature;
    res.status(200).json({ hotelId, files, temp });
  });

The builder is not 100% complete, my intention was to show the approach. I would probably modify it so that a set of middlwares can be used.

Note that it behaves as Builder until handle is called. So it is immutable and can be chained and result reused

Something like this:

const authUserWithWeather = ControllerBuilder.create()
  .addMiddleware(authMiddleware)
  .addMiddleware(weatherMiddleware);


authUserWithWeather.get("/").handle(() => {});
authUserWithWeather
  .addMiddleware(multipartMiddleware)
  .get("/something")
  .handle(() => {})

Link to the demo again: stakc-blitz modified

Svetoslav Petkov
  • 1,117
  • 5
  • 13
  • thank you for the solution, but currently in my code I chain verb handlers and use different middleware functions each : `router().get(handler).post(authMiddleware, handler2)`, how can I scope middlewares to specific method in your solution? and how can I stack multiple methods as shown ? – Ismail_Aj Sep 21 '22 at 16:30
  • You cannot chain handlers with my solution. You cannot pass middlewares as array since the metadata will be lost and the request object will not have the correct type. – Svetoslav Petkov Sep 26 '22 at 07:29
0

I went for a hacky but simple solution in my app where I only have a single change I want to make to the types. I basically have some logic in a middleware function which I know will guarantee a certain property exists on my request object if it makes it past the middleware, so create the router with the request type with that additional property inside the util. It's certainly not generic, but for more simple cases it gets the job done.

import { NextApiRequest, NextApiResponse } from 'next'
import { createRouter } from 'next-connect'
import { getAuth } from 'my-auth'

type NextApiRequestAuthed = NextApiRequest & {
  userId: string
}

export function getAuthRouter<ResponseData = any>() {
  const router = createRouter<NextApiRequestAuthed, NextApiResponse<ResponseData>>()
  router.use(async (req, res, next) => {
    const auth = getAuth(req)
    if (auth.userId == null) {
      return res.status(400)
    }
    req.userId = auth.userId
    await next()
  })
  return router
}

Then it can simply be used like this:

type ResponseData = { data: string }
const router = getAuthRouter<ResponseData>()

router.get(async (req, res) => {
  const { userId } = req /* type of req is NextApiRequestAuthed */
  ...
Hugo
  • 334
  • 3
  • 10