Skip to content

Instantly share code, notes, and snippets.

@ambergristle
Last active January 31, 2025 18:54
Show Gist options
  • Save ambergristle/f4f805138f0d72d7f5e43edf0ffc8c85 to your computer and use it in GitHub Desktop.
Save ambergristle/f4f805138f0d72d7f5e43edf0ffc8c85 to your computer and use it in GitHub Desktop.
The same validation middleware implemented with three approaches, each peeling back a layer of abstraction, to illustrate Hono middleware typing. For more validator-specific Hono middleware, see: https://hono.dev/examples/validator-error-handling#see-also
import type { ValidationTargets } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
// Your custom error formatter
import { formatZodError } from '@/lib/zod-error';
export const customZodValidator = <
// json, form, query, param, header, cookie
Target extends keyof ValidationTargets,
Schema extends z.ZodSchema
>(target: Target, schema: Schema) => {
return zValidator(target, schema, (result, c) => {
// Early-return (or throw) on error
if (!result.success) {
// Error requirements will vary by use-case
return c.json({
timestamp: Date.now(),
message: `invalid ${target}`,
issues: formatZodError(result.error.issues),
}, 400);
}
// Otherwise return the validated data
return result.data;
});
};
import type { ValidationTargets } from 'hono';
// Swap `zValidator for` `validator`
// import { zValidator } from '@hono/zod-validator';
import { validator } from 'hono/validator';
import { z } from 'zod';
// Your custom error formatter
import { formatZodError } from '@/lib/zod-error';
export const customZodValidator = <
Target extends keyof ValidationTargets,
Schema extends z.ZodSchema
>(target: Target, schema: Schema) => {
// return zValidator(target, schema, (result, c) => {
return validator(target, async (value): Promise<z.output<Schema>> => {
// We have to run validation ourselves
const result = await schema.safeParseAsync(value);
if (!result.success) {
return c.json({
timestamp: Date.now(),
message: `invalid ${target}`,
issues: formatZodError(result.error.issues),
}, 400);
}
return result.data;
});
};
import type { ValidationTargets } from 'hono';
import { validator } from 'hono/validator';
// Your custom error formatter
import { formatZodError } from '@/lib/zod-error';
// Any validation function you provide must
// take unknown data and return data of a known type,
// or produce some kind of error
type ValidationFunction<T, E extends Error = Error> = (data: unknown) => (
{ success: true; data: T; }
| { success: false; error: E }
);
export const customAgnosticValidator = <
// json, form, query, param, header, cookie
Target extends keyof ValidationTargets,
T extends Record<string, any>
>(target: Target, validate: ValidationFunction<T>) => {
return validator(target, (value, c) => {
const result = validate(value);
if (!result.success) {
return c.json({
timestamp: Date.now(),
message: `invalid ${target}`,
issues: formatError(result.error.issues),
}, 400);
}
return result.data;
});
};
import type { ValidationTargets } from 'hono';
import { createMiddleware } from 'hono/factory';
/**
* This is `not` the way to validate requests, but
* it gives us insight into how Hono shares data
* between middleware and handlers. It comes in clutch
* for workflows like auth or rate limiting.
*/
const overEngineeredAgnosticValidator = <
Target extends keyof ValidationTargets,
T extends Record<string, unknown>
>(
target: Target,
validate: ValidationFunction<T>
) => {
return createMiddleware<{
Variables: {
validated: Record<Target, T>
}
}>(async (c, next) => {
// Get and format target data from Request
// https://github.com/honojs/hono/blob/b2affb84f18746b487a2e02f0b1cd18e2bd8e5f5/src/validator/validator.ts#L72
const value = await getTargetData(c, target);
const result = validate(value);
if (!result.success) {
return c.json({
timestamp: Date.now(),
message: 'Invalid Payload',
issues: formatError(result.error),
}, 400);
}
const validated = {
// Get previously-validated data
// `c.get('validated')` would also work
...c.var.validated,
[target]: result.data,
};
// Update the validated data in context
c.set('validated', validated);
// Don't forget to await. It's not necessary
// until it is, and then it's a pain to retrofit
await next();
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment