# Frameworks

> Where a semantic call belongs in Next.js, Express, Hono and edge runtimes, and how to keep the key on the server.

Section: Guides · HTML: https://jevascript.org/docs/frameworks · Markdown: https://jevascript.org/docs/frameworks.md

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:

```ts
// lib/semantic.ts
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.

```ts
// app/api/tickets/route.ts
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 })
}
```

```ts
// app/tickets/actions.ts
"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_KEY` in `.env.local` or your host's environment. **Never** prefix it with `NEXT_PUBLIC_`.
- Never import `lib/semantic.ts` from 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](#edge-runtimes) below.
- If your bundler struggles with the package's `.js` ESM output, add it to `serverExternalPackages` in `next.config.ts` so Node loads it directly. This is not normally needed.

## Express, Fastify, Hono, NestJS

Nothing special. Import the shared module inside the handler:

```ts
// server.ts
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:

1. There is no `process.env`. Pass the key explicitly.
2. Create the instance per request or lazily, not at module top level, if your platform forbids I/O during module evaluation.

```ts
// worker.ts
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](https://jevascript.org/docs/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`.

```bash
bun run app.ts
deno run --allow-net --allow-env --env-file app.ts
```

## Background 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 `fetch` wrapper 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](https://jevascript.org/docs/not-a-security-boundary).
