Skip to content

[fetch-router] Proposal: Type-safe extra data from middlewares #10891

@lemol

Description

@lemol

While middlewares can enhance the context with additional data, the data is currently not well-typed. It would be advantageous if the middleware could specify the type of data it adds to the context, so, for example, the requireAuth() middleware would include a well-typed user, and the validateBody(schema) middleware could set the correct, properly typed body.

I have been playing with this idea and created the fetch-router-extra package, which is a helper built on top of fetch-router that introduces a new variation of the Middleware type:

export interface Middleware<
  extra extends Record<string, any> = {},
  method extends RequestMethod | 'ANY' = RequestMethod | 'ANY',
  params extends Record<string, any> = {},
> extends MiddlewareBase<method, params> {}

Alongside util functions that are "type-level only", such as defineController, defineAction, and use, which help ensure accurate type inference.

In this implementation, the middleware must set its data in the context.extra object, which the handler/action can access (example).

To demonstrate how to use it, you can check the bookstore-extra demo (a fork of the bookstore demo). I have also created the following packages on top of the fetch-router-extra:

With this, every middleware data in the following action definition is properly typed, including the user, formData, and services objects:

const settingsUpdate = defineAction(routes.account.settings.update, {
  middleware: use(
    requireAuth(),
    withServices(ServiceCatalog.authService),
    withFormData(
      z.object({
        name: z.string().min(1),
        email: z.string().email(),
        password: z.string().optional(),
      }),
    ),
  ),
  action: async ({ extra }) => {
    let { user } = extra

    let { name, email, password } = extra.formData
    let { authService } = extra.services

    let updateData: Partial<User> = { name, email }
    if (password) {
      updateData.password = password
    }

    await authService.updateUser(user.id, updateData)

    return redirect(routes.account.index.href())
  },
})

Demos:

While we can see that both the new Middleware type and the defineAction/defineController functions can be defined externally (thanks to remix's modularity), I would like to hear your thoughts on adding this extra type-safety at the library level.

Thank you!

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions