Hono’s type system is one of its greatest strengths. If you don’t take some time to understand the basics though, it can prove to be a frustrating barrier to entry. As a newer framework, and one dedicated to flexibility, Hono’s official documentation mainly covers core concepts and base-cases. Dozens of official and community middleware and templates will steer you towards scalable and maintainable solutions, but the patterns and details are largely left to you.
After working for years with meta-frameworks that seem to have an opinion about everything, this feels like a breath of fresh air. Hono’s middleware and helpers can be used out-of-box to meet many projects’ basic requirements, but it’s also remarkably simple to extend or replicate them to meet your project’s specific needs.
Learning Hono hands-on
Hono’s approach to request validation is an ideal case study. Its core hono/validator
implementation is only ~150 lines, half of which are imports and types. The logic itself is really straightforward, and can be trivially extended or modified locally. In fact, Hono’s validator-specific middleware are all built on validator
, and its types.
If you want to get the most out of this article—and Hono—you’ll need to be comfortable with TypeScript and generics. You should be familiar with web API and TypeScript basics, but it’s ok if you’re a beginner, or prefer to use TypeScript sparingly: Hono’s types and utilities will do most of the heavy lifting for us.
To get a clear picture of how Hono middleware works, we’ll implement the same validation middleware using three approaches, each peeling back a layer of abstraction:
- First with
@hono/zod-validator
—the out-of-box solution, - Then with
hono/validator
—if you want to use your own validator, or bake in error processing, - And finally with Hono’s
createMiddleware
—not recommended for production, but a great way to take a closer look at how Hono works.
Hono’s validator
is especially powerful in combination with its RPC client, so we’ll also take a quick look at how route typing plugs into hono/client
. We won’t be covering OpenAPI integration—that deserves its own discussion—but most topics we address will be relevant in any middleware or handler.
Low-lift validation with @hono/zod-validator
If you’re already using a type-safe schema library, like Zod or TypeBox, and you just want to plug your schema in and go, Hono’s got you covered. You just install the relevant package-specific validation middleware, and it handles most of the boilerplate for you.
I’m a long-time Zod fan, so we’ll start with Hono’s zod-validator
, but none of the examples or discussion will delve too deeply into Zod specifics. Instead, we’ll be focusing on what we can learn about Hono’s middleware typing from the package’s internals.
Sharing valid data with Context
The first piece of Hono typing we really need to understand is the Context
object. Whether we’re using one of the dozens of official and community middleware—or creating our own—we’ll be working with Context
. It exposes the app environment—including bindings for Cloudflare environments, the Request
and Response
, and a variety of helpers for reading and writing data. You can read more about those in the Hono Context
API docs.
Crucially, Context
allows us to share data between middleware and handlers type-safely. This can be useful in a number of ways, but we’ll start with the c.req.valid
method, which allows us to access any request data validated by validator
(or middleware like zod-validator
that use it internally).
When we pass zValidator
a target ('query'
) and a schema, the schema’s output becomes type-safely available in the handler (or subsequent middleware). Hono supports six validation targets, representing the most common formats for request data.
json
form
(multipart/form-data
orapplication/x-www-form-urlencoded
)query
param
header
cookie
For simplicity, here we only validate the query string, but you can validate as many targets as you’d like by chaining multiple validators.
Customizing the error hook
The zod-validator
package makes it really easy to enforce type-safety within handlers, but what happens when requests fail validation? By default, zValidator
immediately ends the request, returning a 400
with a body containing the full ZodError
object.
While convenient in development, it’s not great for production. This is especially true if you have internals that need obscuring (like auth flows), if you want to standardize error responses, or if your app has complex error-handling requirements (like logging or alerts).
To override this default behavior, zValidator
accepts a third hook
argument: a callback that exposes the validation result and our good friend Context
. If validation fails, you can send a custom response, or throw to an error handler for additional processing.
While it’s great to have this flexibility, responding to errors consistently makes APIs easier to build, troubleshoot, and work with. By abstracting the hook, we can reuse it whenever we validate, ensuring that invalid requests are handled the same way each time.
This gets tedious quickly though, and can be difficult to maintain. To save ourselves the trouble of injecting our error hook each time we call zValidator
, we can instead bake it into a custom middleware.
As you can see, the implementation is simple enough: zValidator
does the heavy lifting both at compile- and at run-time. We only need a few generics to make zValidator
aware of the argument types passed to our wrapper—essentially we’re prop-drilling the type—and it will take care of communicating with Hono’s type system.
To allow for one-off routes with distinct error-handling requirements, we could also add an optional override hook. The typing for that is a little more complicated though, so we won’t get into that just yet.
More flexibility with hono/validator
First, let’s get a better understanding of how schema output types get from zValidator
to our handlers. Behind the curtain, it’s just a Zod-specific wrapper around validator
, and Hono offers equivalents for Typebox, Typia, and Valibot.
If you don’t see your favorite validator (or parser) on the list, or want to get creative with your error processing, fret not. It’s fairly trivial to reproduce the essentials yourself. Hono tools are built to be extended, and the source code is refreshingly accessible.
Changing only three lines, we can update our example to remove the @hono/zod-validator
dependency, and decouple our logic from Zod. At this level of abstraction, you’re free to validate request data any way you’d like. You can then retrieve valid data in the handler, using c.req.valid
.
How does this actually work though?
When we use validator
, the callback’s (non-Response
) return type gets added to a type map, using the path, method, and target as keys. To achieve this, validator
leverages Hono’s MiddlewareHandler
type. Like Context
, MiddlewareHandler
takes three generic arguments, for Env
, path, and Input
:
Env
and Input
are the type parameters you’ll manually work with the most. Env
exposes any environment variables (Bindings
), along with any values you’ve set
in Context
in your middleware (Variables
), while Input
represents any request data validated using hono/validator
.
This would be the Input
type for our simple search query, for example:
Downstream handlers use the out
type to determine which targets have been validated, and to appropriately type values returned from c.req.valid
. This is essentially Hono’s secret sauce: it uses generics to merge types into a format useable across the request lifecycle.
Using a different validator
All we really need then, is a target and an output type. We can easily update our custom Zod validator to accept a generic parse function (or one from a different library), as long as we make sure validator
generically knows the return type.
This approach is ideal if you want to minimize your dependencies, or if you want to use an unsupported validator. Otherwise, the sturdiest (and most cost-effective) solution is to build on top of an existing package-specific Hono validator.
Getting extra with createMiddleware
To get an even closer look, let’s take things a step too far, and implement our own version of validator
. While you wouldn’t want to do this for your validation layer, it will give us a chance to manually get and set values in Context
, which is really a game-changer for things like auth.
To keep typing simple, we’ll take advantage of Hono’s createMiddleware
helper. This factory method ensures that your middleware typing can be read by subsequent handlers.
Instead of using the Input
type though, we’ll use the Env
type. There’s no way to set the data that’s available on c.req.valid
without using validator
(or forking Hono), but Context
comes with a getter and setter that we can use to type-safely share our own custom data.
Since we’re not using validator
, we won’t be able to access our data in the handler using c.req.valid
. Instead, we’ll use the createMiddleware
type generic to specify that we’ll be setting a validated
property in Context
variables, whose value is our parse result. We could then access our results in the handler like this:
Querying validated endpoints with Hono RPC
If your app has a front-end, Hono’s RPC client is a popular choice for keeping your types synced across your stack. It brings intellisense and type-safety to your request construction, representing resources as objects nested by path and method.
There are a few gotchas to usage though, notably that the RPC client only works with json
and text
responses. If your endpoint doesn’t return either, you can still use the client, but without the benefit of any additional type-safety. Moreover, if an endpoint returns both json
and an incompatible method (e.g, c.req.body
), none of the responses will be inferred.
I haven’t worked with it extensively, so we’ll need to save a more in-depth discussion for another time, but it’s worth getting a sense of how the types inferred from our backend code get used by the client (and how they don’t).
Inferred request and response types
As the client’s behavior suggests, all the (chained) middleware and handler types for an app or route are merged into a type map that’s keyed by endpoint and method, and includes the inferred input and union of output types.
In this case, we see that our posts endpoint requires a search
query value, and returns either a 200
with some data, or a 400
with an error message. Remember that only text
or json
responses returned from the handler will be included. Responses returned from middleware or helpers like notFound
or onError
are not included either. This behavior is not supported by Hono’s current type system, but it’s a known issue that may be addressed in the future.
To get around this, you can explicitly set handler types yourself, though that’s not especially ergonomic, and is somewhat counterintuitive. The best solution will depend on your use-case, but using a standardized error response format combined with some custom type checking should easily bridge the gap for now.
Regardless, it will often also be necessary to additionally parse data client-side: complex objects like Date
are serialized for HTTP requests, and clients generally don’t deserialize them for you. While this might seem cumbersome, it’s fairly trivial to add a validation layer around Hono’s client (or any TypeScript HTTP client) that transforms values as-needed. That, though, is a tomorrow problem.
It’s middleware all the way down
For now, I hope that I’ve left you feeling excited to take your middleware to the next level, and confident to start exploring the Hono source code if you haven’t already! It’s an amazing feat of engineering, and a great resource throughout the development process.
Hono’s flexibility—which extends from its cross-runtime compatibility to its helper methods—opens the door to a highly composable and type-safe architecture. It’s simple to build on, introducing additional complexity and abstraction only as needed.
This approach radically simplifies workflows—like auth and rate limiting—that often require data to be shared between multiple middleware layers before the request even hits the handler.
I’m currently having a lot of fun building auth into a Hono app using the new Lucia Auth guide, which is an awesome resource if you want to roll your own auth, or just learn more.. I’m still ironing out some kinks, but I look forward to publishing an article on Hono auth in the coming months!
Until then, if you need help with Hono or are seeking inspiration for your next project, check out the Hono Discord. I’ve found it to be an incredibly welcoming and supportive community, following the example set by the project authors, maintainers, and contributors.