-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Description
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:
- form-data-typed-middleware: validates/parse the form using a
standardschemalibrary like zod, and includes the parsed data in the context. - router-services-middleware: type-safe dependency injection.
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:
- bookstore-extra: fork of the bookstore demo using form-data-typed and router-services middlewares
- basic router-services: basic usage of the router-services middleware
- complete router-services: basic usage of the router-services middleware
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!