Membrane + Hono API router
2025-01-18This week we're working IRL in Miami for a Membrane team onsite. For our hackathon, I wanted to improve DX for HTTP endpoint routing in Membrane.
Instantly deployed APIs, without a database
Membrane is often a good fit when you want to cobble together a quick API for an small tool. In particular, Membrane programs already come with:
- HTTP endpoints that deploy on save, so you can write some code and test instantly (e.g. using cURL)
- Built-in state, so you can store data without having to configure a database*
Up until now, though, creating routes has been a bit clunky. You might do something like:
export const endpoint = ({ method, path }) => {
switch (`${req.method} ${req.path}`) {
case "GET /":
return "hello world";
case "GET /tacos":
return "tacos";
default:
return JSON.stringify({ status: 404 })
}
}
The endpoint
function has that signature because it's just another Membrane action, but routing requests with a bare switch
statement is very limiting and can quickly get messy.
What we'd been missing is a proper router like express
or hono
. Most routers take a standard Request and return a standard Response, so we need a convenient way to convert endpoint
arguments into a Request
and serialize a Response
.
Membrane + Hono
Hono as been picking up steam lately, and for good reason. Here's that endpoint rewritten in Hono:
import { Hono } from "hono";
import { withRequestResponse } from "membrane-utils";
const app = new Hono();
app.get("/", (c) => c.html("hello world"));
app.get("/tacos", (c) => c.html("tacos"));
export const endpoint = withRequestResponse(app.fetch);
We're introducing a new withRequestResponse
higher-order function that converts the arguments of endpoint
into a Request
object, passes it to Hono (or any router), and serializes the router's Response
. I published an example here that you can install in your workspace: pete/hono-taco
Example: feature flag service
Let's say you want to create a simple feature flag service, e.g. for an internal dashboard that your customer support team uses. With Hono and built-in state, a program like this takes just a few minutes to set up: pete/hono-flags
import { state } from "membrane";
import { Hono } from "hono";
import { withRequestResponse } from "membrane-utils";
const app = new Hono();
// Expose the API from your program HTTP endpoint
export const endpoint = withRequestResponse(app.fetch);
// Store feature flags in Membrane `state`
state.flags ??= [];
export interface State {
flags: Array<{
id: string;
name: string;
description: string;
createdAt: string;
enabled: boolean;
}>;
}
// List all feature flags
app.get("/flags", (c) => c.json(state.flags));
// Get a single feature flag
app.get("/flags/:id", (c) => {
const id = c.req.param("id");
const flag = state.flags.find((flag) => flag.id === id);
if (!flag) {
return c.json({ error: "Flag not found" }, 404);
}
return c.json(flag);
});
// Create a new feature flag
app.post("/flags", async (c) => {
const body = await c.req.json();
if (!body.name || !body.description) {
return c.json({ error: "Name and description are required" }, 400);
}
const newFlag: FeatureFlag = {
id: crypto.randomUUID(),
name: body.name,
description: body.description,
createdAt: new Date().toISOString(),
enabled: body.enabled ?? false,
};
state.flags.push(newFlag);
return c.json(newFlag, 201);
});
// Update a single feature flag
app.patch("/flags/:id", async (c) => {
const id = c.req.param("id");
const body = await c.req.json();
const index = state.flags.findIndex((flag) => flag.id === id);
if (index === -1) {
return c.json({ error: "Flag not found" }, 404);
}
state[index] = {
...state[index],
...body,
id: state[index].id,
createdAt: state[index].createdAt,
};
return c.json(state[index]);
});
// Enable a feature flag
app.post("/flags/:id/enable", (c) => {
const id = c.req.param("id");
const index = state.flags.findIndex((flag) => flag.id === id);
if (index === -1) {
return c.json({ error: "Flag not found" }, 404);
}
state[index].enabled = true;
return c.json(state[index]);
});
// Disable a feature flag
app.post("/flags/:id/disable", (c) => {
const id = c.req.param("id");
const index = state.flags.findIndex((flag) => flag.id === id);
if (index === -1) {
return c.json({ error: "Flag not found" }, 404);
}
state[index].enabled = false;
return c.json(state[index]);
});
// Delete a feature flag
app.delete("/flags/:id", (c) => {
const id = c.req.param("id");
const index = state.flags.findIndex((flag) => flag.id === id);
if (index === -1) {
return c.json({ error: "Flag not found" }, 404);
}
state.flags = state.flags.filter((flag) => flag.id !== id);
return c.json({ message: "Flag deleted successfully" });
});
Routing with Hono improves the DX of whipping up a quick API in Membrane. Feel free to install these packages in your workspace and tailor them to your needs. And if you have any requests or questions getting set up, don't hesitate to reach out via email or on Discord.
*While our JavaScript runtime is what enables the magic of state, it does come with the occasional compatibility gap. We're making progress on closing those gaps so that you can write JavaScript that just works in Membrane. Reach out if you run into any JS features or npm packages that don't work as expected.
- Pete Millspaugh (pete@membrane.io)