1. Setup and key
Install the official openai package — there is no separate NormaHub client, compatibility comes from switching the baseURL. Keep the key in the process environment variables and never commit .env to the repository. Russian version of this guide: AI API на Node.js.
npm install openai
export NORMAHUB_API_KEY="your_api_key"Create an API key in the NormaHub dashboard and copy the value once: it is not shown again. Issue separate keys with their own spend limits for different environments (dev, prod, a specific bot).
2. First request
The client works like the original OpenAI: the same create signature, the same choices. Only two lines of configuration differ — apiKey from the environment and the NormaHub baseURL. Cap the answer length with max_tokens: it guards against runaway dialogs and controls the budget.
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.NORMAHUB_API_KEY,
baseURL: "https://api.normahub.cc/v1",
});
const response = await client.chat.completions.create({
model: "MODEL_ID",
messages: [{ role: "user", content: "Hello" }],
max_tokens: 512,
});
console.log(response.choices[0].message.content);Pass the model as the exact ID from the current catalog. After the first successful answer, cross-check usage and cost in the dashboard history — a successful HTTP response does not yet mean correct billing.
3. Streaming responses
Streaming mode delivers tokens as they generate: the interface comes alive instantly and the user never stares at a spinner. Handle an interruption separately from a normal finish — part of the answer may already have reached the user while the server considers the operation done.
const stream = await client.chat.completions.create({
model: "MODEL_ID",
messages: [{ role: "user", content: "Explain briefly" }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}Streaming is a property of the specific endpoint and model. If streaming is not supported, use the regular mode instead of endless retries. Event and completion details are in the streaming guide.
4. Error handling
The SDK throws typed exceptions: AuthenticationError for 401, RateLimitError for 429, APIError with a status field for the rest. Split permanent errors (400, 401, 402, 403 — fix the cause) from temporary ones (429, 5xx — retry with backoff). Always set a timeout: a hanging request is worse than a fast error.
import OpenAI from "openai";
try {
await client.chat.completions.create({
model: "MODEL_ID",
messages: [{ role: "user", content: "Hello" }],
timeout: 30_000,
});
} catch (error) {
if (error instanceof OpenAI.AuthenticationError) {
console.error("401: key invalid or revoked");
} else if (error instanceof OpenAI.RateLimitError) {
console.error("429: key limit hit, retry later");
} else if (error instanceof OpenAI.APIError) {
console.error(`HTTP ${error.status}: see the errors guide`);
} else {
throw error;
}
}Save the X-Request-Id of every request — support finds the operation by it without your key or prompt. The full code table and retry policy are in the API errors guide.
5. Key security
The key must stay on the server. For a browser application, create your own protected backend route: the frontend sends the message to your server, the server attaches the secret key and calls NormaHub. Along the way, trim the input message length and cap max_tokens — otherwise someone else's script through your form will spend the whole balance.
// app/api/chat/route.ts — the key lives on the server only
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.NORMAHUB_API_KEY,
baseURL: "https://api.normahub.cc/v1",
});
export async function POST(request: Request) {
const { message } = await request.json();
const response = await client.chat.completions.create({
model: "MODEL_ID",
messages: [{ role: "user", content: String(message).slice(0, 4000) }],
max_tokens: 512,
});
return Response.json({ text: response.choices[0].message.content });
}Additionally, rate-limit your own route and log only statuses and request IDs, never keys or content. When errors appear, check the balance and available models in the dashboard.