-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Take advantage of Middleware / route typings in separate files #708
Comments
There's probably something I don't understand here. Why would you want to pass a "partial" route to If the idea is to use the const restrictedRoute = route.use(verifySessionMiddleware)
const route1 = restrictedRoute
.get('/user/:id')
.use(isSelfOrAdmin)
.handler(async (req) => {
return findUserController(req.foo, somethingElse)
})
const route2 = restrictedRoute
.get('/other/path')
.use(isSelfOrAdmin)
.handler(async (req) => {
return findUserController(req.bar, otherStuff)
}) What comes to export type IsSelfOrAdminMiddleware = Middleware.ChainedMiddleware<
SessionWrapper & { params: { foo: string, bar: number } },
unknown,
Response.Forbidden<ApiHttpErrorPayloadDTO>
> |
Mostly because in a real world application, having the route paths scattered in 50 files is a pretty bad idea. So I'd want to centralise them right in the router code. Thus the "partial route" (RouteConstructor) seems the way to go.
Good point there. |
After digging, I think exporting publicly Do you mind if I PR this ? Another solution would be to export JUST the handler from the controller file. But that would require a great type definition for it. I need to explorer this one |
Centralising paths, middleware, etc. makes a lot of sense! But you could achieve this by making your route handlers small so that they only pass the data from the request to one (or a few) functions, and then create a response based on what those functions return. So instead of moving the route handler elsewhere, move the logic to separate functions and keep the route handlers small. |
@akheron thanks for the suggestion. Though, I'd still have the trouble to have to "receive" the route typings (and so, re-construct it) in the route handler. Here is my current solution : export const baseRoute = route.useParamConversions(customConversions)
export type BaseRouteFn = typeof baseRoute
export type BaseRouteConstructor<
Path extends string = string,
TAdditionalRequestProps extends object = object,
> = MakeRoute<RequestBase & TAdditionalRequestProps, CustomConversions, Path> export const _sessionRoute = (appContext: AppContext) =>
baseRoute
// Check account with supertokens
.use(verifySessionMiddleware())
// Fetch user by account
.use(_sessionUserMiddleware(appContext))
export type SessionRouteFn = ReturnType<typeof _sessionRoute>
export type SessionRouteConstructor<
Path extends string = string,
TAdditionalRequestProps extends object = object,
> = BaseRouteConstructor<
Path,
SessionWrapper & SessionUser & TAdditionalRequestProps
> const v1Router = (appContext: AppContext) => {
const sessionRoute = _sessionRoute(appContext)
// Bind routes
const _v1Router = router(
// Signed-in User
findUserMeController(appContext)(sessionRoute.get('/user/me'))
)
} const findUserMeController: Controller<
SessionRouteConstructor,
Response.Ok<UserPayloadDTO> | Response.NotFound<ApiHttpErrorPayloadDTO>
> = () => (baseRoute) =>
baseRoute.handler(async (request) => {
// User already extracted from session
// by sessionUserMiddleware above.
// It would SELECT it or throw 404
const { sessionUser } = request
return Response.ok(sessionUser)
}) This works like a charm, but when I want to retrieve some typed request params; I have to re-specify the path in the handler. const findTenantController: Controller<
SessionRouteConstructor<'/:id(uuidv4)'>, // <= HERE
Response.Ok<TenantPayloadDTO> | Response.NotFound<ApiHttpErrorPayloadDTO>
> = (appContext) => (baseRoute) =>
baseRoute.handler(async (request) => {
//...
}) I think exporting internal |
I'd like to chime in here, because I once faced the same issue. What I ended up doing was following @akheron 's advice of making your route handlers small so that they only pass the data from the request to one (or a few) functions. What I did was create a generic For example, based on the following middleware, I purposely abstract their type, so it can be imported later on: export type AuthCtx = { user: Pick<User, "id"> };
const authMiddleware: Middleware.Middleware<AuthCtx, never> = () => {
// ...
return Middleware.next({ user: { id: 3 } });
};
export type BodyCtx<T extends z.ZodTypeAny> = { body: z.infer<T> };
const bodyParserMiddleware = <T extends z.ZodTypeAny>(schema: T): Middleware.Middleware<BodyCtx<T>, never> =>
async (ctx) => {
try {
const body = await schema.parseAsync(ctx.req.body);
return Middleware.next({ body });
} catch {
throw new HttpBadRequestError();
}
}; Then I construct my route, which quickly delegates all heavy work to the product service layer and passes down the request context. export const bodyShape = z.object({ name: z.string() })
const myRoute = route
.post("/products")
.use(authMiddleware, bodyParserMiddleware(bodyShape))
.handler(async (ctx) => {
const product = await productService.create(ctx);
return Response.created(product);
}); And finally, the function create(ctx: RequestCtx<AuthCtx | BodyCtx<typeof bodyShape>>) {
// ...
return db.product.create({ userId: ctx.user.id, ...ctx.body });
} This is how the import type { RequestBase } from "typera-express";
type UnionToIntersectionHelper<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type UnionToIntersection<U> = UnionToIntersectionHelper<U> extends infer O
? { [K in keyof O]: O[K] }
: never;
export type RequestCtx<T = {}> = RequestBase & UnionToIntersection<T>; |
Hey !
Hacking around; I'm facing a challenge with middleware typings. Here's my need expressed :
I would like to "split" the construction of my routes in several parts, so that I have a file for the router, and another file for each route. Basic.
I came up with the following theorical implementation, which would be super sexy :
Router
verifySessionMiddleware
This is a
wrap
ped one I made over Supertokens. Basically it writesrequest.session
or throws401
And this is where I begin struggling.
My end goal is to provide strong typings
isSelfOrAdminMiddleware
findUserController
himselfI'll begin with the controller
Example "controller" :
findUserController
isSelfOrAdminMiddleware
To sumup, my whole point is roughly to benefit the typings from
route.[method]('/path/:id')
both in a further middleware and in the final handler.Any idea ? :)
Thanks !
The text was updated successfully, but these errors were encountered: