Advanced HTTP Endpoints: Convex ❤️ Hono
Convex supports HTTP actions, meaning your backend can receive requests not only from Convex clients, such as the ConvexReactClient, but also from third-party webhooks and other clients that want to communicate with a custom HTTP API.
Currently, these endpoints have a simple router. In this post, we’ll look at how to add more advanced features, such as:
- Dynamic routes or slug routes — e.g.
users/:userId
- Middleware — e.g. check auth on all routes under
/api/*
or implementing CORS - Helpers for validating an incoming Request’s query params or body
- Helpers for formatting a JSON response or text response
- Custom 404 (Not Found) responses
While it’s possible to build these yourself on top of Convex primitives, existing JS libraries already do a great job at this. In this post, we’re going to go through how you can leverage Hono with Convex HTTP actions. To see the implementation, check out hono.ts
in the convex-helpers package. We’ll also look more generally at how to extend Convex HTTP endpoint behavior.
Note: you don’t need to use TypeScript, but I will use it in my examples because both Hono and Convex offer slick TypeScript support!
Using Hono with Convex
To use Hono, you’ll need to:
npm install hono convex-helpers
in your project.- In
convex/http.ts
, importHono
,HonoWithConvex
,HttpRouterWithHono
, andActionCtx
1:
import { Hono } from "hono";
import { HonoWithConvex, HttpRouterWithHono } from "convex-helpers/server/hono";
import { ActionCtx } from "./_generated/server";
const app: HonoWithConvex<ActionCtx> = new Hono();
// Add your routes to `app`. See below
export default new HttpRouterWithHono(app);
The HonoWithConvex
is just a type that tells Hono to expect the Convex ActionCtx
as its env binding. HttpRouterWithHono
does the magic to connect Hono routes to Convex HTTP actions.
Let’s look at a few ways to use Hono:
Slug routing and response formatting
For illustration, we’ll implement an endpoint from the Convex demo for HTTP action to use Hono. Here’s an example handler showcasing several of Hono’s features:
// Routing with slugs
app.get("/listMessages/:userId{[0-9]+}", async (c) => {
// Extracting a token from the URL!
const userId = c.req.param("userId");
// Running a Convex query
const messages = await c.env.runQuery(api.messages.getByAuthor, { authorNumber: userId });
// Helpers for pretty JSON!
c.pretty(true, 2);
return c.json(messages);
});
…and an example response:
$ curl https://happy-animal-123.convex.site/listMessages/123
[
{
"_creationTime": 1677798437141.091,
"_id": {
"$id": "messages|lqMHm5kDS9m6fBsSnx5L2g"
},
"author": "User 123",
"body": "Hello world"
},
]
Input validation
Here’s another handler with input validation:
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";
app.post(
"/postMessage",
// Body validation!
zValidator(
"json",
z.object({
author: z.string().startsWith("User "),
body: z.string().max(100),
})
),
async (c) => {
// With type safety!
const { body, author } = c.req.valid("json");
await c.env.runMutation(api.messages.send, { body, author });
return c.text("Sent message!");
}
);
…and an example response:
$ curl -d '{ "body": "Hello world", "author": "123" }' https://happy-animal-123.convex.site/postMessage
{
"success": false,
"error": {
"issues": [
{
"code": "invalid_string",
"validation": {
"startsWith": "User "
},
"message": "Invalid input: must start with \"User \"",
"path": [
"author"
]
}
],
"name": "ZodError"
}
}
Middleware: Adding CORS
Another example, copying from Hono docs. This adds CORS support to the /api/*
and /api2/*
routes with different configurations.
import { cors } from 'hono/cors'
...
app.use('/api/*', cors())
app.use(
'/api2/*',
cors({
origin: 'http://examplesite.com',
allowHeaders: ['X-Custom-Header', 'Upgrade-Insecure-Requests'],
allowMethods: ['POST', 'GET', 'OPTIONS'],
exposeHeaders: ['Content-Length', 'X-Kuma-Revision'],
maxAge: 600,
credentials: true,
})
)
The .use
function registers a handler for all /api/*
requests. As we’ll see below, you can use this for a variety of situations, including logging.
Custom 404 responses
To set up a custom 404, we can do:
// Custom 404
app.notFound(c => {
return c.text("Oh no! Couldn't find a route for that", 404);
});
See https://hono.dev/ for more features.
Convex is the sync platform with everything you need to build your full-stack project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.
Under the hood
Curious about how to extend Convex HTTP actions for your own purposes? Read on!
Extending routes using the /
prefix
If the routing options aren’t flexible enough for your use case, you can handle all HTTP requests with a single httpAction
and do complex routing there. For instance, instead of using HttpRouterWithHono
, we could define a single route per HTTP method in convex/http.ts
:
import { httpRouter, ROUTABLE_HTTP_METHODS } from "convex/server";
import { httpAction } from "./_generated/server";
import { HonoWithConvex } from "./lib/honoWithConvex";
const app: HonoWithConvex = new Hono();
// Add your routes to `app`.
const http = httpRouter();
for (const routableMethod of ROUTABLE_HTTP_METHODS) {
http.route({
pathPrefix: "/",
method: routableMethod,
handler: httpAction(async (ctx, request: Request) => {
return await app.fetch(request, ctx);
}),
})
}
export default http;
We could stop here — we can now use Hono and Convex together! But we could make a couple of additional improvements to leverage the Convex dashboard.
Using middleware to add per-route logging
Here’s what we see in the Convex dashboard under “Logs” given the approach of registering an httpAction
per method:
Logs showing GET /*
All our GET requests will appear as GET /*
even when we have multiple routes.
We can pretty easily get a little more information using one of Hono’s features — logging middleware:
import { logger } from "hono/logger";
import stripAnsi from "strip-ansi";
app.use(
"*",
logger((...args) => {
console.log(...args.map(stripAnsi));
})
);
Now the Convex dashboard looks more like this:
Logs with GET /listMessages/123
Note: these say 0ms
because they’re running in Convex’s deterministic environment that provides a different Date.now()
.
Subclassing HttpRouter
(the HttpRouterWithHono
approach)
If we want the fullest integration with the Convex dashboard, we’d like to see something like this under “Logs”, where we show the routed path:
Logs showing GET /listMessages/:userId
And then see a corresponding entry with metrics under the “Functions” tab:
Functions metrics for GET /listMesssages/:userId
The code needed for this behavior is in honoWithConvex.ts
.
How does it work?
HttpRouterWithHono
is a subclass of the Convex HttpRouter
which overrides two special methods:
getRoutes
returns a list of[path, method, handler]
, which we use to populate the Functions page on the dashboard.lookup(path, method)
returns[handler, path, method]
. Convex will runhandler
when responding to the request and use thepath
andmethod
for metrics and logging (so this should match a path + method combo fromgetRoutes
)- As an example, I wanted
lookup("/listMessages/123", "GET")
to return"/listMessages/:userId{[0-9]+}"
for the path and"GET"
for the method.
- As an example, I wanted
The implementation I added is not optimal (it loops through all the routes), but it still works! The Convex router is very flexible, so there are many options for configuring how your HTTP actions get routed and show up in the dashboard.
Now, we can use Convex with Hono and take advantage of most of the features provided in the Convex dashboard!
Summary
In this post, we looked at how to use Hono with Convex, including how to extend Convex’s HTTP actions to add your own functionality. Let us know what you think in our discord and if you end up using Hono and Convex together! ❤️
Footnotes
-
HonoWithConvex
andActionCtx
are just being used for types. If you're using JavaScript, you can ignore that import and just initializeapp
asconst app = new Hono();
↩
Convex is the sync platform with everything you need to build your full-stack project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.