Guides
Frameworks
Where a semantic call belongs in Next.js, Express, Hono and edge runtimes, and how to keep the key on the server.
The package is plain ESM with no dependencies, so it needs no framework integration. Three things matter everywhere: the call runs on the server, the instance is created once, and the key never reaches a client bundle.
The shared module#
Create the instance once and import it. This is the same shape as a database client:
import { createSemantic, jev } from "jevascript"
export const semantic = createSemantic({
provider: jev(),
defaults: { timeoutMs: 5_000, cache: "10m" },
})
export const triage = semantic.defineSchema({ /* ... */ })Next.js#
Call it from anything that runs on the server: route handlers, server actions, server components, middleware on the Node runtime.
import { triage } from "@/lib/semantic"
export async function POST(request: Request) {
const ticket = await request.json()
const t = await triage(ticket)
return Response.json({ priority: t.urgency > 80 ? "high" : "normal", team: t.department })
}"use server"
import { triage } from "@/lib/semantic"
export async function submitTicket(form: FormData) {
const t = await triage({ subject: form.get("subject"), body: form.get("body") })
// ...
}- Put
JEV_API_KEYin.env.localor your host's environment. Never prefix it withNEXT_PUBLIC_. - Never import
lib/semantic.tsfrom a client component. If you need a judgement in the browser, expose a route handler or a server action. - The Node runtime is required for the decision call. On a route where you have set
export const runtime = "edge", see Edge runtimes below. - If your bundler struggles with the package's
.jsESM output, add it toserverExternalPackagesinnext.config.tsso Node loads it directly. This is not normally needed.
Express, Fastify, Hono, NestJS#
Nothing special. Import the shared module inside the handler:
import express from "express"
import { triage } from "./lib/semantic.js"
const app = express().use(express.json())
app.post("/tickets", async (req, res) => {
const t = await triage(req.body)
res.json({ priority: t.urgency > 80 ? "high" : "normal", team: t.department })
})Load the environment before the first call. With Node 22, node --env-file=.env server.ts is enough.
Edge runtimes#
Cloudflare Workers, Vercel Edge Functions and Deno Deploy all provide fetch. The package additionally needs AbortSignal.any and AbortSignal.timeout; check your runtime's compatibility date supports both.
Two adjustments:
- There is no
process.env. Pass the key explicitly. - Create the instance per request or lazily, not at module top level, if your platform forbids I/O during module evaluation.
import { createSemantic, jev } from "jevascript"
export default {
async fetch(request: Request, env: { JEV_API_KEY: string }) {
const semantic = createSemantic({ provider: jev({ apiKey: env.JEV_API_KEY }) })
const body = await request.json()
const urgent = await semantic(body).is("A human needs to act on this urgently.")
return Response.json({ urgent })
},
}The in-memory cache is per isolate on the edge. For a shared cache, plug in your platform's KV store through the two-method SemanticCache interface. See Caching.
Bun and Deno#
Both run the package unchanged. Bun reads .env automatically; Deno needs --allow-net and --allow-env, and reads .env with --env-file.
bun run app.ts
deno run --allow-net --allow-env --env-file app.tsBackground jobs and queues#
Semantic calls are I/O; treat them like any other. A worker that processes a queue benefits most from batching many questions per item, and from caching, so a retried job returns the same decision as the first attempt.
Where the key must never go#
- A client bundle (anything under
"use client", a Vite app, a browser script). - A public environment variable prefix (
NEXT_PUBLIC_,VITE_,PUBLIC_). - Logs. The provider never logs the key; make sure your
fetchwrapper does not either.
If you need a decision in the browser, put a server endpoint in front of it, with rate limiting and input size limits, and treat the input as hostile. See Not a security boundary.