Stack logo
Sync up on the latest from Convex.
Sarah Shader's avatar
Sarah Shader
2 years ago

Advanced HTTP Endpoints: Convex ❤️ Hono

Defining a Hono endpoint in Convex

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:

  1. npm install hono convex-helpers in your project.
  2. In convex/http.ts, import Hono, HonoWithConvex, HttpRouterWithHono, and ActionCtx1:
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.

Build in minutes, scale forever.

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.

Get started

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 /*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/123Logs 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/:userIdLogs showing GET /listMessages/:userId

And then see a corresponding entry with metrics under the “Functions” tab:

Functions metrics for GET /listMesssages/:userIdFunctions 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 run handler when responding to the request and use the path and method for metrics and logging (so this should match a path + method combo from getRoutes)
    • As an example, I wanted lookup("/listMessages/123", "GET") to return "/listMessages/:userId{[0-9]+}" for the path and "GET" for the method.

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

  1. HonoWithConvex and ActionCtx are just being used for types. If you're using JavaScript, you can ignore that import and just initialize app as const app = new Hono();

Build in minutes, scale forever.

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.

Get started