H
Hono2w ago
Ego

Handler type

Hey guys was wondering, what is the best way to implement a RouteHandler like the one present in @hono/zod-openapi based om my code below, I am using hono-openapi for .lazy() and a couple other features
31 Replies
Ego
EgoOP2w ago
File, yes it's a TSX because Discord mobile things .TS is a media file
ambergristle
ambergristle2w ago
Can you share a snippet or a link to GitHub? Opening files from internet strangers is kinda sus
Ego
EgoOP2w ago
GitHub
Custom RouteHandler type · honojs · Discussion #4087
Hey guys was wondering, what is the best way to implement a RouteHandler like the one present in @hono/zod-openapi based on my function below createRoute, I am using hono-openapi for .lazy() and a ...
ambergristle
ambergristle2w ago
thanks! i'll take a look i was on mobile + didn't realize that the file was embedded; my bad @Ego you mean this? https://github.com/honojs/middleware/blob/6c36f525f9682909196eaffd620e30a6a1f34b76/packages/zod-openapi/src/index.ts#L318 Can you share a bit more about what your goal is? It seems like you’re creating a factory to construct handlers based mainly on schema inputs And what issues are you running into?
Ego
EgoOP2w ago
I basically want to do something like that yeah, my experience with typing inference related stuff is close to none so I honestly just don't know where to start
ambergristle
ambergristle2w ago
I’d recommend taking a look at the factory helpers And spend some time studying the hono source code But tbh it’s a pretty ambitious project if you’re not already comfortable w generics, and it seems like an overly rigid way to use hono
Ego
EgoOP2w ago
My problem could be simplified quite a bit if I wasn't using the speerate definitions.ts and handlers.ts files and I would just register stuff right away tho it would make my code way less readable tbh so I end up with stuff like this
export const index = createRoute({
method: 'GET',
path: 'items',
tags: ['Items'],
operationId: 'listItems',
request: {
query: PaginationQuery.extend({
orderBy: z
.optional(z.string().pipe(z.enum(['-createdAt', 'createdAt', '-updatedAt', 'updatedAt'])))
.default('-updatedAt'),
}).merge(filterSchema),
},
responses: {
200: {
content: {
'application/json': {
schema: resolver(getPaginatedSchema(selectItemsSchema)),
},
},
description: 'List of items logged in the database',
},
},
});
export const index = createRoute({
method: 'GET',
path: 'items',
tags: ['Items'],
operationId: 'listItems',
request: {
query: PaginationQuery.extend({
orderBy: z
.optional(z.string().pipe(z.enum(['-createdAt', 'createdAt', '-updatedAt', 'updatedAt'])))
.default('-updatedAt'),
}).merge(filterSchema),
},
responses: {
200: {
content: {
'application/json': {
schema: resolver(getPaginatedSchema(selectItemsSchema)),
},
},
description: 'List of items logged in the database',
},
},
});
export const show: RouteHandler<typeof ShowType> = async (c) => {
const params = c.req.valid("param");

const [{ amount }] = await c.var.db
.select({
amount: sql`GREATEST(SUM(${schema.item_logs.amount}), 0)`.mapWith(Number),
})
.from(schema.item_logs)
.where(eq(schema.item_logs.itemId, params.item.id));

return c.json({ ...params.item, amount }, 200);
};
export const show: RouteHandler<typeof ShowType> = async (c) => {
const params = c.req.valid("param");

const [{ amount }] = await c.var.db
.select({
amount: sql`GREATEST(SUM(${schema.item_logs.amount}), 0)`.mapWith(Number),
})
.from(schema.item_logs)
.where(eq(schema.item_logs.itemId, params.item.id));

return c.json({ ...params.item, amount }, 200);
};
I have come up with this but it feels incredibly incorrect and honestly kinda disgusting and it still doesn't do the return types correctly
import type { Context, Env } from "hono";
import type { RouteOutput } from "./routing";
import type { z, ZodType } from "zod";

type MaybePromise<T> = T | Promise<T>;

type ExtractZodSchema<T> = T extends ZodType ? T : never;

type ValidKeys = "query" | "param" | "header" | "json" | "form";

type InputFromRequest<Request> = {
param: Request extends { params: infer P }
? z.output<ExtractZodSchema<P>>
: never;
query: Request extends { query: infer Q }
? z.output<ExtractZodSchema<Q>>
: never;
header: Request extends { headers: infer H }
? z.output<ExtractZodSchema<H>>
: never;
json: Request extends {
body: { content: { "application/json": { schema: infer Schema } } };
}
? z.output<ExtractZodSchema<Schema>>
: never;
form: Request extends {
body: { content: { "multipart/form-data": { schema: infer Schema } } };
}
? z.output<ExtractZodSchema<Schema>>
: never;
};

type MergeInput<I extends Record<string, any>> = {
[K in keyof I]: I[K];
};

type ExtractResponseContent<R> = R extends { responses: infer Responses }
? {
[K in keyof Responses]: Responses[K] extends { content: infer Content }
? Content
: never;
}[keyof Responses]
: never;

type ValidatedContext<
E extends Env,
I extends Record<string, any>,
> = Context<E> & {
req: {
valid: <K extends ValidKeys>(key: K) => I[K];
};
};

export type RouteHandler<
R extends RouteOutput<any, any, any, any, any, any>,
E extends Env = Env,
> = (
c: ValidatedContext<E, MergeInput<InputFromRequest<R["request"]>>>,
) => MaybePromise<ExtractResponseContent<R["description"]>>;
import type { Context, Env } from "hono";
import type { RouteOutput } from "./routing";
import type { z, ZodType } from "zod";

type MaybePromise<T> = T | Promise<T>;

type ExtractZodSchema<T> = T extends ZodType ? T : never;

type ValidKeys = "query" | "param" | "header" | "json" | "form";

type InputFromRequest<Request> = {
param: Request extends { params: infer P }
? z.output<ExtractZodSchema<P>>
: never;
query: Request extends { query: infer Q }
? z.output<ExtractZodSchema<Q>>
: never;
header: Request extends { headers: infer H }
? z.output<ExtractZodSchema<H>>
: never;
json: Request extends {
body: { content: { "application/json": { schema: infer Schema } } };
}
? z.output<ExtractZodSchema<Schema>>
: never;
form: Request extends {
body: { content: { "multipart/form-data": { schema: infer Schema } } };
}
? z.output<ExtractZodSchema<Schema>>
: never;
};

type MergeInput<I extends Record<string, any>> = {
[K in keyof I]: I[K];
};

type ExtractResponseContent<R> = R extends { responses: infer Responses }
? {
[K in keyof Responses]: Responses[K] extends { content: infer Content }
? Content
: never;
}[keyof Responses]
: never;

type ValidatedContext<
E extends Env,
I extends Record<string, any>,
> = Context<E> & {
req: {
valid: <K extends ValidKeys>(key: K) => I[K];
};
};

export type RouteHandler<
R extends RouteOutput<any, any, any, any, any, any>,
E extends Env = Env,
> = (
c: ValidatedContext<E, MergeInput<InputFromRequest<R["request"]>>>,
) => MaybePromise<ExtractResponseContent<R["description"]>>;
ambergristle
ambergristle2w ago
What do you mean by “doesn’t do the return types correctly”?
Ego
EgoOP2w ago
It's not inferring the return c.json Always ends up in a never
ambergristle
ambergristle2w ago
Using a higher-order function might help a bit. Then you can pass the handler itself as a callback Treating validators as a tuple might also free you up a bit, but I’m just riffing My first goal would be to get rid of most of the custom types. They’re really going to slow down the TS server The HOF will help w that; leaning more on hono helpers might as well
Ego
EgoOP2w ago
Yeah I'm pretty sure I should be using the hono types directly, tbh I'm just a bit clueless and this is actually way too complicated for me to start with It's just a bit sad that the official zod-openapi middleware uses a package underneath that is abandonware Otherwise it would work perfectly
ambergristle
ambergristle2w ago
Have you tried hono-openapi? It’s newer, and the typing works a bit differently, but if nothing else it could be a helpful reference
Ego
EgoOP2w ago
That's exactly what I'm using it just handles things way different and doesn't provide generic types for this That's why I made the createRoute factory to try and be as plug and play as possible
ambergristle
ambergristle2w ago
Gotcha. The createApp factory paired with the createHandlers helper might be what you’re looking for I think that one of the issues you’re running into though is Hono’s own typing. It’s good-enough for (mostly) nice DX, but it does some funky things that make it hard to extend, or work with in complicated ways
Ego
EgoOP2w ago
I'm pretty sure I could use hono's own handler type if I knew more but I'm sadly not smart enough
ambergristle
ambergristle2w ago
From a learning perspective, a simpler starting point might be to create a custom validation middleware that applies all the relevant validators
Ego
EgoOP2w ago
That would work if I defined my route in the function itself
ambergristle
ambergristle2w ago
Maybe. I wouldn’t be so sure. Regardless, it’s just a matter of practice, so you’re on the right track!
Ego
EgoOP2w ago
Unfortunately that would clutter my files incredibly If I did use a new Hono on each file etc it could work but then separation of concerns would be an annoying thing
ambergristle
ambergristle2w ago
I like to create each route (sometimes each endpoint) in its own file. Each has its own Hono instance, and most of the code is inline I only abstract custom middleware and db boilerplate
Ego
EgoOP2w ago
Yeah that would simplify my life a lot but at the same time I personally don't like how the approach looks
ambergristle
ambergristle2w ago
No? What don’t you like about it? (Not that you have to)
Ego
EgoOP2w ago
It feels awkward, I come from a Laravel background and I really like how it handles most stuff
ambergristle
ambergristle2w ago
Interesting. I really like it because everything that matters for each handler is in one place, and I find it easy to read, modify, and maintain IMO it’s a really clean way to write JS
Ego
EgoOP2w ago
That makes sense as long as you don't share a lot of code like schemas etc
ambergristle
ambergristle2w ago
My schemas live in their own directory As do helpers, utils, and anything else shared I keep handlers specifically scoped to their own implementation concerns
Ego
EgoOP2w ago
Makes sense if they're not owned by a specific resource
ambergristle
ambergristle2w ago
Also makes it easy to extend them, or share them with other schemas
Ego
EgoOP2w ago
That resource has 4 or 5 methods that might or might not slightly change the way the schema looks
ambergristle
ambergristle2w ago
Sounds like that resource could be 4 or 5 resources with their own schema But I also burned myself out on abstractions and complex types last year, lol
Ego
EgoOP2w ago
Yeah I can see that The complex types in this specific use case do feel perfect for my style

Did you find this page helpful?