Server Handler recipes
Mount createGridCueHandler in Next.js, Hono, Cloudflare Workers, Express, or plain Node.
The Server Handler holds the Intent Provider's key and answers resolution requests from the browser. createGridCueHandler builds it:
import { createGridCueHandler, createJevProvider } from "gridcue/server";
const handler = createGridCueHandler({
provider: createJevProvider({ apiKey: process.env.JEV_API_KEY }),
});
// handler: (request: Request) => Promise<Response>It is a Fetch-standard function, so it runs anywhere a Request to Response function runs. For Node's (req, res) style, wrap it with toNodeHandler.
In the browser, point createRemoteProvider({ endpoint: "/api/gridcue" }) at the path you mount.
What the handler does
- Accepts only
POST(else 405) with a JSONcontent-type(else 415). - Refuses bodies over
maxBodyBytes, default 32 KB (413). It stops reading as soon as the limit is passed. - Validates the body against the resolution protocol (400 on anything else).
- Calls the provider, validates its answer, and returns it as JSON with
cache-control: no-store. - On a provider failure, returns 502 with a stable code such as
PROVIDER_FAILEDorPROVIDER_TOO_COMPLEX, never the provider's own message. - Never logs requests, payloads, or keys.
Errors have the shape { "error": { "code": "…", "message": "…" } }. createRemoteProvider reads the code, so the User sees the right message.
It adds no auth and no rate limit. Add both around it: see Protect the endpoint.
Next.js route handler
// app/api/gridcue/route.ts
import { createGridCueHandler, createJevProvider } from "gridcue/server";
export const POST = createGridCueHandler({
provider: createJevProvider({ apiKey: process.env.JEV_API_KEY }),
});The GridCue examples run this on the Node.js runtime, the default.
Hono
Hono gives you the standard Request as c.req.raw:
import { Hono } from "hono";
import { createGridCueHandler, createJevProvider } from "gridcue/server";
const handler = createGridCueHandler({
provider: createJevProvider({ apiKey: process.env.JEV_API_KEY }),
});
const app = new Hono();
app.post("/api/gridcue", (c) => handler(c.req.raw));
export default app;Cloudflare Workers
A Worker reads secrets from env, which only exists inside fetch. Build the handler on the first request and reuse it:
import { createGridCueHandler, type GridCueHandler, createJevProvider } from "gridcue/server";
let handler: GridCueHandler | undefined;
export default {
async fetch(request: Request, env: { JEV_API_KEY: string }): Promise<Response> {
if (new URL(request.url).pathname !== "/api/gridcue") return new Response("Not found", { status: 404 });
handler ??= createGridCueHandler({ provider: createJevProvider({ apiKey: env.JEV_API_KEY }) });
return handler(request);
},
};Store the key with wrangler secret put JEV_API_KEY.
Checked with a live Jev request under wrangler dev (ADR 0011): the TypeSafe SDK runs under workerd, with or without the nodejs_compat flag.
Express
toNodeHandler turns the Fetch-standard handler into a Node (req, res) listener:
import express from "express";
import { createGridCueHandler, createJevProvider, toNodeHandler } from "gridcue/server";
const handler = createGridCueHandler({
provider: createJevProvider({ apiKey: process.env.JEV_API_KEY }),
});
const app = express();
app.post("/api/gridcue", toNodeHandler(handler));
app.listen(3000);It works with or without express.json() in front: it reads the parsed req.body when a body parser already ran, and the raw stream otherwise. It also aborts the provider call when the client disconnects.
toNodeHandler(handler, { maxBodyBytes }) takes its own body limit, default 32 KB, for the stream it reads itself.
Plain Node
The same listener mounts in node:http, Connect, Fastify's raw handlers, or Vite's dev server:
import { createServer } from "node:http";
import { createGridCueHandler, createJevProvider, toNodeHandler } from "gridcue/server";
const listener = toNodeHandler(
createGridCueHandler({ provider: createJevProvider({ apiKey: process.env.JEV_API_KEY }) }),
);
createServer((req, res) => {
if (req.url === "/api/gridcue") return void listener(req, res);
res.writeHead(404).end();
}).listen(3000);For Vite's dev server, see Vite with TanStack Table.
Where gridcue/server loads
gridcue/server resolves by export condition:
| Condition | Resolves to |
|---|---|
workerd (Cloudflare Workers) | The real module |
edge-light (Vercel Edge Runtime) | The real module |
browser | A stub that throws on import |
default (Node and the rest) | The real module |
The browser stub is deliberate. If a client file imports gridcue/server, the build fails loudly instead of shipping a key-holding module to the browser. worker is deliberately not a condition, because bundlers also use it for a page's own Web Workers (ADR 0011).
In the browser, use createRemoteProvider from gridcue instead.
Any other provider
createGridCueHandler takes any IntentProvider, not only Jev. The Mock Provider works for keyless staging environments:
import { createMockProvider } from "gridcue/mock";
export const POST = createGridCueHandler({ provider: createMockProvider() });To write your own, see Build a provider.