H
Hono3mo ago
Sun

Extending the Context type

So, I've made a function to work to with @arrirpc/schema (yes, it's a validator), so I can use it like this
export const routes = new Hono()
.get("/", async (c) => {
return respondWith(c, HealthSchema, {
status: 200,
title: "Health Check",
data: {
message: "Health is OK!",
}
});
});
export const routes = new Hono()
.get("/", async (c) => {
return respondWith(c, HealthSchema, {
status: 200,
title: "Health Check",
data: {
message: "Health is OK!",
}
});
});
The "problem" is that it feels kinda weird, having to pass in the context that way. It does't really feel like the rest of the hono stuff (respondWith(c, schema, data) vs c.respondWith(schema, data) or even better, as a middleware, but I don't know how to do that on hono) Is there a way I can make it better user experience or do I need to create a validator middleware there?
19 Replies
ambergristle
ambergristle3mo ago
hard to say exactly without knowing how respondWith is implemented but hono's createMiddleware helper might do the trick
Sun
SunOP3mo ago
import { a, ASchema } from "@arrirpc/schema";
import { problemResponse } from "@/responses/problem";
import { Context } from "hono";


export function respondWith<
TOk extends ASchema,
T extends ReturnType<typeof problemResponse<TOk>> = never,
TSchema extends a.infer<T["raw"]> = never,
>(c: Context, problem: T, data: TSchema) {
const response = problem.of(data);

if (!response.success) {
c.status(417);
return c.json({
status: 417,
title: "Validation Error",
errors: response.errors,
} as TSchema);
}

c.status(response.value.status as any);
return c.json(response.value);

}
import { a, ASchema } from "@arrirpc/schema";
import { problemResponse } from "@/responses/problem";
import { Context } from "hono";


export function respondWith<
TOk extends ASchema,
T extends ReturnType<typeof problemResponse<TOk>> = never,
TSchema extends a.infer<T["raw"]> = never,
>(c: Context, problem: T, data: TSchema) {
const response = problem.of(data);

if (!response.success) {
c.status(417);
return c.json({
status: 417,
title: "Validation Error",
errors: response.errors,
} as TSchema);
}

c.status(response.value.status as any);
return c.json(response.value);

}
whatever tf this is don't ask how I did this, I have no idea
import { a, ASchema } from "@arrirpc/schema";

export function problemResponse<T extends ASchema>(successSchema: T) {
const $responseSchema = a.object({
status: a.number(),
title: a.string(),
data: a.optional(successSchema),
errors: a.optional(
a.array(
a.object({
message: a.string(),
instancePath: a.string(),
schemaPath: a.optional(a.string()),
data: a.optional(a.any()),
})
)
)
});

const responseSchema = a.compile($responseSchema);


return {
raw: $responseSchema,
model: responseSchema,
of<T extends a.infer<typeof $responseSchema>>(data: T) {
return responseSchema.parse(data);
}
}
}
import { a, ASchema } from "@arrirpc/schema";

export function problemResponse<T extends ASchema>(successSchema: T) {
const $responseSchema = a.object({
status: a.number(),
title: a.string(),
data: a.optional(successSchema),
errors: a.optional(
a.array(
a.object({
message: a.string(),
instancePath: a.string(),
schemaPath: a.optional(a.string()),
data: a.optional(a.any()),
})
)
)
});

const responseSchema = a.compile($responseSchema);


return {
raw: $responseSchema,
model: responseSchema,
of<T extends a.infer<typeof $responseSchema>>(data: T) {
return responseSchema.parse(data);
}
}
}
That's the problemSchema thingy, btw, don't also ask how I did it
ambergristle
ambergristle3mo ago
yeah, just use createMidleware
const respondWith = (problem, data) => {
return createMiddleware(async (c, next) => {
const response = problem.of(data)
// ...
return c.json(response.value)
})
}

app.use(
respondWith(HealthSchema, {
status: 200,
title: "Health Check",
data: {
message: "Health is OK!",
}
})
)
const respondWith = (problem, data) => {
return createMiddleware(async (c, next) => {
const response = problem.of(data)
// ...
return c.json(response.value)
})
}

app.use(
respondWith(HealthSchema, {
status: 200,
title: "Health Check",
data: {
message: "Health is OK!",
}
})
)
Sun
SunOP3mo ago
But that wouldn't rly work though, as I have no control over the data at the middleware level id I were to do middlewares, I'd need to somehow validate after sending stuff, dunno and if I did that, the types would kinda need to match, but then I'd have no context (thx javascript)
ambergristle
ambergristle3mo ago
ah, right, it's a handler, my bad same principle though (higher order function)
const respondWith = (problem, data) => {
return async (c: Context) => {
// ...
}
}
const respondWith = (problem, data) => {
return async (c: Context) => {
// ...
}
}
stryche.
stryche.3mo ago
TS Playground - An online editor for exploring TypeScript and JavaS...
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
Sun
SunOP3mo ago
can we get back to prototype stuff, I could just do Context.prototype.validated = (data) => ... :d
stryche.
stryche.3mo ago
it's just simple
// middleware.ts
declare module "hono" {
export interface Context {
respondWith: typeof respondWith
}
}

export const responseWithMiddleware = () => createMiddleware(async (ctx, next) => {
ctx.respondWith = respondWith.bind(ctx)
await next();
})
// middleware.ts
declare module "hono" {
export interface Context {
respondWith: typeof respondWith
}
}

export const responseWithMiddleware = () => createMiddleware(async (ctx, next) => {
ctx.respondWith = respondWith.bind(ctx)
await next();
})
export const routes = new Hono()
.use("*", responseWithMiddleware())
.get("/", async (c) => {
return c.respondWith(HealthSchema, {
status: 200,
title: "Health Check",
data: {
message: "Health is OK!",
}
});
});
export const routes = new Hono()
.use("*", responseWithMiddleware())
.get("/", async (c) => {
return c.respondWith(HealthSchema, {
status: 200,
title: "Health Check",
data: {
message: "Health is OK!",
}
});
});
Sun
SunOP3mo ago
what happens if I forget the .use("*", responseWithMiddleware()) line? bc that's 100% gonna happen
ambergristle
ambergristle3mo ago
using c.set/c.get is probably preferred then your tests will fail
Sun
SunOP3mo ago
no idea what that is and I don't really care about best practices rn what tests :d
Sun
SunOP3mo ago
jk, but then I don't really thing I'd like that I'll try to do a middleware later, something like the zValidator I've seen, dunno I hate ts types though, dunno how I'm making that work
stryche.
stryche.3mo ago
you can just use this, it's only register the typings when you import the file,.. i think, if you didn't use the middleware it makes the ctx.respondWith undefined
Sun
SunOP3mo ago
nono, ts makes it global, so even if I don't have that line registers it, it'll still think it's there and the types are gonna be messed up anyways, that's a future me problem
stryche.
stryche.3mo ago
welp, just don't forget to use it,. if you get undefined then you've forgot to use the middleware put the middleware at the top level, so it will call before anything else
Sun
SunOP3mo ago
that's a future prod problem, lol
stryche.
stryche.3mo ago
and, i think chained type on middleware isn't possible?
ambergristle
ambergristle3mo ago
yeah. if you use c.set (and correctly type + chain the middleware) then downstream handlers will only be able to (get typed) access the stored value if the middleware exists on the route

Did you find this page helpful?