Creuto is now an OpenAI Select Partner Read More
Use the Jev JavaScript SDK in Node and Next.js: typed questions, SystemOneRequest, answers typed by criteria, error classes, why the key stays server-side.

The Jev JavaScript SDK ships with a client option called dangerouslyAllowBrowser. It defaults to false, and the docs describe what turning it on does in plain words: it allows browser use, exposing the API key to page users. That single flag tells you most of what you need to know about where a Jev call belongs in a Node or Next.js app.
This guide covers the TypeScript side of Jev as of 23 September 2026: installing @typesafe-ai/sdk, building a SystemOneRequest, using the typed question interfaces so that answers arrive typed rather than parsed, placing the call inside a Next.js route handler, and catching the right error class when the API says no. Signatures are taken from the SDK's published API reference.
Jev's HTTP endpoint is one POST. You could call it with fetch in about fifteen lines. What you would then write by hand is the part that matters: a type for every answer shape, a discriminator on type, retry and backoff, Retry-After handling, and an error class per status code.
The SDK's actual selling point is narrower and better than "it wraps fetch". Answer types are inferred from your questions. Define a choice over the labels billing, technical and other, and answers.category.choice is typed as exactly those three strings — the ChoiceResponse interface declares choice as keyof T & string, where T is the criteria object you passed in. A typo in a downstream comparison is a compile error, not a branch that silently never runs.
The package is @typesafe-ai/sdk and it needs Node.js 20 or newer. It publishes ESM, CommonJS and TypeScript declarations, so it drops into a Next.js app or an Express service without a build shim.
npm install @typesafe-ai/sdk
import { choice, TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient();
const response = await client.systemOne({
state: { document: "I was charged twice. Please fix this ASAP." },
questions: {
category: choice("What is this ticket about?", {
billing: null,
technical: null,
other: null,
}),
},
});
console.log(response.answers.category.choice);
new TypeSafeClient() with no arguments reads TYPESAFE_API_KEY from the environment and defaults defaultModel to jev-latest and baseURL to https://api.typesafe.ai. The constructor throws if the key is missing, the configuration is invalid, or the runtime is unsupported — a boot-time failure rather than a request-time one, which is the behaviour you want from a module-scope client.
Two defaults deserve attention before they surprise you in production. timeout is 10,000 milliseconds per attempt, with no total retry budget across attempts — a point where the JavaScript SDK differs from its Python sibling, which caps the whole call at 30 seconds. And logLevel defaults to warn; raising it to debug adds headers and bodies to your logs, and while credential headers are redacted, bodies are not. Your state is the body.
A call takes one object. The SystemOneRequest interface has exactly three properties: state, which is text, a JSON object or array, or null; questions, a non-empty map keyed by names you choose; and an optional model override that falls back to the client's defaultModel. A second argument, RequestOptions, carries per-call timeout, retry, headers and cancellation.
Your question names become the answer keys, so name them the way the consuming code reads. systemOne returns an APIPromise<SystemOneResult<Q>>, and the result carries three things: answers, typed per question; model, the versioned ID that actually answered, which is worth logging because jev-latest is an alias that moves; and usage, with input_tokens and output_tokens as plain numbers.
Three helpers build the three typed questions, and each returns the matching interface:
| Helper | Builds | Answer interface and fields |
|---|---|---|
choice(instructions, criteria) | ChoiceQuestion<T> | ChoiceResponse: choice, confidence, probabilities |
score(instructions, criteria) | ScoreQuestion<T> | ScoreResponse: score, confidence, legend, probabilities |
noul(instructions?, criteria?) | NoulQuestion | NoulResponse: noul only |
That last row is the one that catches people. NoulResponse declares two properties, type and noul, and noul is the probability of a yes from zero to one. There is no confidence on it. Reading answers.isUrgent.confidence will not compile, and in JavaScript without types it quietly yields undefined, which compares false against every threshold you set.
The criteria shapes differ too. choice takes labels mapped to descriptions, with null for a label you do not want to describe. score takes at least two descriptions indexed by score from zero, and passing fewer throws. noul takes an optional { true, false } pair describing each outcome. A ScoreResponse.score is an expected value and may fall between integer rubric levels, so score >= 2 is a legitimate test and score === 2 is not.
On the server, in a route handler or a server action — never from a component that ships to the browser. Next.js is explicit that environment variables are only available on the server by default, and that anything prefixed NEXT_PUBLIC_ is inlined into the JavaScript bundle at next build. Name your key NEXT_PUBLIC_TYPESAFE_API_KEY and you have published it to every visitor, permanently, in a built artefact. Use TYPESAFE_API_KEY and read it only from code that runs in a route handler.
// app/api/triage/route.ts
import { NextResponse } from "next/server";
import { choice, noul, TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient(); // reads TYPESAFE_API_KEY on the server
export async function POST(request: Request) {
const { message } = await request.json();
const { answers } = await client.systemOne({
state: { message },
questions: {
department: choice("Which team should handle this", {
billing: "Payment or subscription issues",
technical: "Bugs or integration problems",
sales: "Pricing or account questions",
}),
isUrgent: noul("The message conveys urgency or time-sensitivity"),
},
});
return NextResponse.json({
department: answers.department.choice,
confident: answers.department.confidence >= 0.7,
urgent: answers.isUrgent.noul > 0.5,
});
}
Three habits we hold to in our own builds. Construct the client once at module scope rather than per request, so connection reuse is not thrown away. Never return the raw answers object to the browser — return the decision your server made, because shipping probabilities to the client invites someone to re-derive the threshold in front-end code where nobody reviews it. And keep the question text and the thresholds in one module that a reviewer can open: the questions are the business logic here, not the plumbing around them.
One limit worth stating plainly: the SDK documents Node.js 20 or newer, and we run it in the Node.js runtime. We have not tested it on an edge runtime, so we do not claim it works there.
Every HTTP failure arrives as APIError or one of its subclasses, all descending from TypeSafeError. APIError carries status, body (parsed JSON, response text, or undefined when empty), headers, and requestId from the x-typesafe-request-id header. The subclasses are BadRequestError, AuthenticationError, PermissionDeniedError, NotFoundError, UnprocessableEntityError, RateLimitError and InternalServerError. Outside that branch sit APIConnectionError, APITimeoutError and APIUserAbortError.
RateLimitError is the one to handle by hand. It adds retryAfterMs, the server's requested delay, or undefined when the header is absent or invalid. TypeSafe publishes Jev 1.13 limits of 250,000 tokens per second and 1,200 requests per minute, notes that these adjust dynamically while demand is high, and returns 429 on either. That is a number to pass into your own queue rather than to swallow, and the reasoning is the same as when we wrote about what a 429 is actually telling you.
Below your catch block, the SDK has already tried. RetryPolicy defaults to maxRetries: 2, backoffInitialMs: 500 doubling to backoffMaxMs: 5000, jitter of 0.25, retryable statuses 408, 429 and 500–599, and respectRetryAfter: true capped by maxRetryAfterMs: 60000. Override it partially on the client or per call; unset fields inherit. And note the two throws that are not HTTP at all: empty questions, and score criteria that are not a list of at least two entries. Both are your bug, and both fail before a request goes out.
The fair counter-argument is that a TypeScript team already has a typed LLM client and a Zod schema, and adding a second AI vendor buys a type system it already has. That argument holds whenever the output is text. It stops holding when the output is a decision, because a schema validates the shape of an answer while Jev's answer is a shape by construction — and it arrives with a calibrated probability you can threshold instead of a confident sentence you cannot.
It is also the wrong tool for a whole class of question. TypeSafe's own jaggedness page for jev-1.13 says the model does not count reliably, reads dates as text rather than as ordered quantities, is not a calculator, and is not trained to generate text. Keep arithmetic, ordering and counting in TypeScript, where they belong and where they are testable. The broader comparison is in our piece on when a decision model beats an LLM inside a product.
If you are adding this to an existing Next.js app, do the security pass before the feature pass: confirm the key is server-only, confirm nothing logs the request body, and confirm the route that calls Jev is authenticated like any other endpoint that costs money per request. That is the same checklist we apply to any third-party key in a web application we build, and the wider version of it is in our notes on API key governance. The feature takes an afternoon; the key leak takes a rotation and an incident note.
Install @typesafe-ai/sdk on Node.js 20 or newer, set TYPESAFE_API_KEY in the server environment, construct a TypeSafeClient, and call systemOne with a state and a map of named questions. The promise resolves to answers keyed by those names, plus the model and token usage.
Yes. The package ships TypeScript declarations alongside ESM and CommonJS builds, and answer types are inferred from the questions you pass. A choice answer is typed to the exact label set you defined, so a mistyped comparison downstream fails at compile time.
In a route handler or server action, never in client-side code. Next.js only exposes environment variables to the browser when they are prefixed NEXT_PUBLIC_, and those values are inlined into the built JavaScript bundle, so a key named that way is published to every visitor.
No. ChoiceResponse and ScoreResponse both carry confidence and a probability distribution, but NoulResponse exposes only the noul value, a probability of yes from zero to one. Reading confidence off a noul answer yields undefined in plain JavaScript and fails to compile in TypeScript.
It retries twice by default on statuses 408, 429 and 500 to 599, with 500 ms backoff doubling to 5,000 ms and jitter applied. It honours Retry-After up to 60 seconds. A surviving 429 throws RateLimitError, which exposes retryAfterMs for your own backpressure.
Ten thousand milliseconds per attempt, with no total budget across retries. That differs from the Python SDK, which applies a 30-second budget to the whole call including backoff delays. Set the timeout per call through RequestOptions when one route is interactive and another is a batch job.
Ready to take the first step towards unlocking opportunities, realizing goals, and embracing innovation? We're here and eager to connect.
11th Floor, O-Hub, Chandaka Industrial Estate, Infocity, Bhubaneswar, Odisha 751024
Level 4, 11 York Street Sydney Startup Hub Sydney, NSW – 2000
30 N. Đinh Nghệ, Phước Mỹ Sơn Trà, Đà Nẵng / Da Nang City – 550000
Level 25, AIDP Business Tower, Dubai Marina, United Arab Emirates
50 Beauchamp Street, Wellington, WGN 5028, New Zealand