Last active
January 31, 2025 18:54
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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