How to Call a Free LLM API in Node.js (OpenAI SDK)
If you are looking for a free LLM API Node.js can talk to without a billing account, DreamPrompting is an OpenAI-compatible gateway at https://dreamprompting.com/api/v1. It routes your request across several providers and fails over automatically when one of them is slow or down. Because the wire format is the OpenAI protocol, the only change to existing JavaScript code is the base URL and the key.
You do need an API key, and it is free. Sign in with Google on the account page, create a key, and you are done. No credit card, no waitlist. The key goes in an Authorization: Bearer header, which the official openai npm package handles for you. Below are five things worth having in your project: a first call, streaming, a zero-dependency version, error handling that actually matches what the gateway returns, and an Express route.
Install the openai npm package
One dependency covers every SDK example here. The package is the same client you would use against OpenAI, so nothing about it is DreamPrompting specific.
npm install openai
The examples use ESM syntax and top-level await, so either add "type": "module" to your package.json or name your files with a .mjs extension. If you are stuck on CommonJS, swap the import for const OpenAI = require("openai") and wrap the calls in an async function. Node 18 or newer is assumed throughout, mostly because the fetch example depends on it.
Your first free LLM API JavaScript call
Two options matter when you construct the client: baseURL and apiKey. Read the key from the environment rather than pasting it into the file, for reasons covered further down.
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://dreamprompting.com/api/v1",
apiKey: process.env.DREAM_API_KEY,
});
const response = await client.chat.completions.create({
model: "auto",
messages: [
{ role: "system", content: "You are a concise assistant." },
{ role: "user", content: "Give me three ideas for a weekend Node project." },
],
});
console.log(response.choices[0].message.content);
Run it with DREAM_API_KEY=your_key node app.mjs and you should see a reply within a second or two. Passing model: "auto" hands model selection to the gateway, which picks something available and retries elsewhere if a provider errors out. When you want determinism instead, pin a provider/model string such as groq/openai/gpt-oss-120b for speed or google/gemini-3.1-flash-lite for cheap long-context work. The live lineup is on the models page, and GET /api/v1/models returns the same list as JSON when you send your key.
Streaming with for await
Waiting for a full completion feels sluggish in a CLI or a chat UI. Set stream: true and the SDK returns an async iterable of OpenAI-format server-sent event chunks, which you consume with for await.
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://dreamprompting.com/api/v1",
apiKey: process.env.DREAM_API_KEY,
});
const stream = await client.chat.completions.create({
model: "auto",
messages: [{ role: "user", content: "Explain the Node event loop in four sentences." }],
stream: true,
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}
process.stdout.write("\n");
The optional chaining is not decoration. The final chunk usually carries a finish_reason and no content, and some providers emit a role-only first chunk, so reading delta.content without a guard will hand you undefined and print it. Using process.stdout.write instead of console.log avoids a newline after every token. If you want to pipe this straight through to a browser, the transport details are in the streaming guide, and you can watch it behave in the chat playground first.
Zero dependencies with native fetch
Node 18 ships a global fetch, so you can skip npm entirely. This is the version to use in a Lambda, a Cloudflare Worker, a Vercel edge function, or any place where bundle size or a locked-down dependency list is a real constraint.
const res = await fetch("https://dreamprompting.com/api/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.DREAM_API_KEY}`,
},
body: JSON.stringify({
model: "auto",
messages: [{ role: "user", content: "Name one underrated Node core module." }],
}),
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) {
throw new Error(`API error ${res.status}: ${await res.text()}`);
}
const data = await res.json();
console.log(data.choices[0].message.content);
Two details are easy to forget. First, fetch does not reject on a 4xx or 5xx response, so you have to check res.ok yourself. Second, there is no default timeout, and a hung request will hold your process open indefinitely. AbortSignal.timeout is the shortest fix. The same pattern works for the read-only endpoints too, for example a GET to https://dreamprompting.com/api/v1/quota with just the auth header returns how much of your daily allowance is left. Every endpoint and parameter is documented in the API docs.
Error handling that matters
Three status codes account for nearly everything you will see in practice. A 401 means the key is missing, malformed, or revoked, and no amount of retrying will fix it, so fail loudly. A 429 means you crossed one of two limits: the burst limit of 100 requests per minute, or your rolling 24 hour account quota of 5,000 requests and 500,000 tokens. A 502 means the gateway tried its providers and all of them failed, which is usually transient. Only the last two are worth a retry.
const RETRYABLE = new Set([429, 502, 503]);
async function withRetry(fn, retries = 3, baseDelay = 500) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
const status = err.status ?? err.response?.status;
if (status === 401) {
throw new Error("Invalid DREAM_API_KEY. Create one at /account.");
}
const transient = status === undefined || RETRYABLE.has(status);
if (!transient || attempt >= retries) throw err;
const wait = baseDelay * 2 ** attempt + Math.floor(Math.random() * 200);
await new Promise((resolve) => setTimeout(resolve, wait));
}
}
}
const response = await withRetry(() =>
client.chat.completions.create({
model: "auto",
messages: [{ role: "user", content: "Summarise HTTP caching in one paragraph." }],
})
);
console.log(response.choices[0].message.content);
Errors thrown by the openai package expose a numeric status, so the same helper wraps both SDK and fetch calls if you normalise the latter into a thrown error. A status of undefined generally means a socket or DNS problem rather than an HTTP response, which is also worth one retry. The exponential backoff with a little jitter keeps a burst of parallel workers from resynchronising and hammering the limit again. Note that the SDK already retries some failures on its own; pass maxRetries: 0 to the constructor if you want your helper to be the only thing retrying. Every response also carries X-RateLimit-Remaining and X-RateLimit-Reset headers, which are cheaper to read than discovering the limit by hitting it.
A practical Express route
Here is the shape most small projects actually need: a server-side route that takes a prompt from your own frontend, calls the gateway, and returns text. Your key never leaves the server.
import express from "express";
import OpenAI from "openai";
const app = express();
app.use(express.json());
const client = new OpenAI({
baseURL: "https://dreamprompting.com/api/v1",
apiKey: process.env.DREAM_API_KEY,
});
app.post("/api/ask", async (req, res) => {
const prompt = String(req.body?.prompt ?? "").trim();
if (!prompt) {
return res.status(400).json({ error: "prompt is required" });
}
try {
const completion = await client.chat.completions.create({
model: "auto",
messages: [
{ role: "system", content: "Answer in at most three sentences." },
{ role: "user", content: prompt },
],
});
res.json({ answer: completion.choices[0].message.content });
} catch (err) {
console.error("LLM call failed:", err.status, err.message);
const status = err.status === 429 ? 429 : 502;
res.status(status).json({ error: "The model is unavailable right now." });
}
});
app.listen(3000, () => console.log("listening on http://localhost:3000"));
Test it with curl -X POST localhost:3000/api/ask -H "Content-Type: application/json" -d '{"prompt":"hello"}'. Notice that a 401 from the gateway is deliberately not forwarded to the client. That is your configuration bug, not theirs, and leaking it tells an attacker something about your setup. Passing the 429 through is useful, since your frontend can then back off. If you are wiring this into a tool-calling loop or an autonomous worker instead of a request handler, the agents page covers the patterns that differ.
Keep your key out of the browser
This is the one part of the guide that is not optional. Put the key in a .env file, add .env to .gitignore, and load it at startup.
# .env
DREAM_API_KEY=your_key_here
# load it, option A: dotenv
npm install dotenv
node -r dotenv/config app.js
# load it, option B: Node 20.6+, no dependency
node --env-file=.env app.mjs
Now the honest warning. Anything that runs in a browser is public. If you construct the OpenAI client in React, Vue, or a plain script tag, the key ships to every visitor, appears in the network tab, and gets scraped within hours. The dangerouslyAllowBrowser flag in the SDK exists to make you think twice, not to make it safe. Bundler prefixes such as NEXT_PUBLIC_ or VITE_ mean exactly the opposite of secret; they mark a value as inlined into client code. The fix is always the same: keep the key on a server and expose your own endpoint, like the Express route above, or a Next.js route handler. Add your own rate limiting per user there, because your daily quota is shared across everyone who finds the endpoint. If a key does leak, revoke it and issue a new one from the account page.
Next steps
You now have a free LLM API JavaScript setup that works with the official SDK, streams token by token, runs with zero dependencies when it needs to, retries the failures worth retrying, and keeps the key server side. From here, the API docs list every parameter the gateway accepts, and the models page lets you compare and test models before you pin one. If Python is also in your stack, the Python version of this guide mirrors it closely, this post on OpenAI-compatible endpoints explains what portability buys you, and the 2026 roundup is worth a look if you are still comparing options. Grab a key on the account page and the first example should run on the first try.