Examples · Web Development
Your first API call in JavaScript
Install Node.js and the OpenAI library, put your key in an environment variable, and ask Qwen3.8 a question from a .mjs file, first in one piece and then streamed word by word. Your code checks that the key is there and that the answer was not cut off.
Beginner · 15 min
What you need
- Node.js 22 or newer. Check with
node --versionin a terminal. If the command is not found, or the number is lower than 22, install the LTS version from nodejs.org. - Your ITS INCOM AI account.
This is the JavaScript twin of Your first API call in Python. The other JavaScript examples start from here.
1. Create a key
At my.ai.itsincom.org, open API keys and create a key. Copy it straight away: it is shown only once.
A key is like a password: do not share it, and never put it in code you publish (GitHub included) or in a web page. Every call uses your credits.
2. A folder and the library
mkdir first-call
cd first-call
npm install openai
npm answers added 1 package and creates package.json and a node_modules folder. ITS INCOM AI speaks the same language as OpenAI: you use their library and change only the address and the key.
3. Put the key in an environment variable
macOS and Linux:
export API_KEY="sk-..."
Windows (PowerShell):
$env:API_KEY = "sk-..."
The variable lives only in that terminal window. Open a new one, and you set it again. Run the program from the same window where you set it.
4. The program
Save it as first.mjs in the first-call folder. The .mjs ending tells Node that the file is an ES module: it can use import, and await outside a function.
import OpenAI from "openai";
if (!process.env.API_KEY) {
console.error("API_KEY is not set: see step 3.");
process.exit(1);
}
const client = new OpenAI({
baseURL: "https://api.ai.itsincom.org/v1",
apiKey: process.env.API_KEY,
});
const answer = await client.chat.completions.create({
model: "qwen3.8-27b",
messages: [
{ role: "system", content: "Answer in English, briefly." },
{ role: "user", content: "Write a JavaScript function that checks whether a string is a palindrome." },
],
});
const choice = answer.choices[0];
console.log(choice.message.content.trim()); // qwen3.8-27b may start with blank lines
console.log("finish_reason:", choice.finish_reason);
console.log("tokens used:", answer.usage.total_tokens);
Run it:
node first.mjs
What you should see
The words change from run to run. One of ours, after about a second:
```javascript
function isPalindrome(str) {
const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '');
return cleaned === cleaned.split('').reverse().join('');
}
```
**How it works:**
1. Lowercases the string and strips non-alphanumeric characters (so `"A man, a plan, a canal: Panama"` works).
2. Compares the cleaned string to its reverse.
**Usage:**
```javascript
isPalindrome("racecar") // true
isPalindrome("hello") // false
isPalindrome("A man a plan a canal Panama") // true
```
finish_reason: stop
tokens used: 198
The answer is Markdown: the backticks and the stars are for a page that turns them into code boxes and bold text. In a terminal you see them as they are.
5. The same, streamed
Without streaming, your program waits in silence until the whole answer is ready. With stream: true, the answer arrives in small pieces, and you print each piece as it comes. This is what chat pages do.
Save it as stream.mjs:
import OpenAI from "openai";
if (!process.env.API_KEY) {
console.error("API_KEY is not set: see step 3.");
process.exit(1);
}
const client = new OpenAI({
baseURL: "https://api.ai.itsincom.org/v1",
apiKey: process.env.API_KEY,
});
const started = Date.now();
const stream = await client.chat.completions.create({
model: "qwen3.8-27b",
messages: [
{ role: "system", content: "Answer in English, briefly." },
{ role: "user", content: "Write a JavaScript function that checks whether a string is a palindrome." },
],
stream: true,
});
let written = "";
let firstWords = null;
let finish = null;
for await (const chunk of stream) {
const choice = chunk.choices[0];
let text = choice.delta.content ?? "";
if (written === "") text = text.trimStart(); // skip the blank lines at the start
if (text && firstWords === null) firstWords = Date.now() - started;
process.stdout.write(text);
written += text;
if (choice.finish_reason) finish = choice.finish_reason;
}
console.log(`\n\nfirst words after ${firstWords} ms, the whole answer after ${Date.now() - started} ms`);
console.log("finish_reason:", finish);
Run it with node stream.mjs. The text appears a few words at a time. At the end, one of our runs said:
first words after 285 ms, the whole answer after 792 ms
finish_reason: stop
for awaittakes the pieces (chunks) one by one, as they arrive from the network.- Each chunk carries a few characters in
delta.content. The first chunk carries only the role, socontentis missing:?? ""makes it an empty string. process.stdout.writeprints without a new line, so the pieces join up.console.logwould put each one on its own line.finish_reasonarrives only in the last chunk, with no text.
A short answer is ready in under a second either way. A long one is where streaming matters: when we asked for “about 400 words on how a web browser loads a page”, the first words came after 0.8–0.9 seconds, and the whole answer after 10 to 16 seconds. Without streaming, the same question meant 12 seconds of silence.
What the program checks
- That the key is there. Without the check, the library stops with
Missing credentials. Please pass an apiKey … or set the OPENAI_API_KEY … environment variable.That message names a variable you never used, and it confuses people. Your own message says what to do. - That the answer is complete.
finish_reasonisstopwhen the model finished, andlengthwhen the answer was cut by a limit (max_tokens). A cut answer can look fine and stop in the middle of a function: check it before you use the text. - Not that the code is right. The model proposes; you check. Copy the function into a file and try it on «I topi non avevano nipoti», a famous Italian palindrome.
Good to know
- Qwen3.8 thinks before it answers. You do not see the thinking, but it counts in the tokens. When we asked it to say hello in Italian in one word, the answer was «Ciao», and it used 33 completion tokens. If you set
max_tokens, leave room (1,000 or more), or the answer may be cut before it starts. - The blank lines. Its answer can start with
\n\n. That is why the first program usestrim(), and the second skips the white space at the start. - No token count in the stream. The school's API does not send one in streaming today: we tried
stream_options: { include_usage: true }, and no count arrived. Your usage is on your account page. - A
.envfile, if you prefer. Node 22 can read the variables from a file: writeAPI_KEY=sk-...in a file called.env, and runnode --env-file=.env first.mjs. Add.envto your.gitignorebefore your first commit, or the key ends up on GitHub. - Never in the browser. JavaScript that runs in a web page is visible to everyone who opens it, key included. A page talks to a small server of yours, and the server holds the key: see A chat box on your web page.
- When something goes wrong (a wrong key, too many requests, a slow answer), see Errors, retries and timeouts in JavaScript.
Exercise
- Change the system message and see how the answer changes.
- Add
temperature: 0, thentemperature: 1: what happens when you run the program several times? - In
stream.mjs, ask for something long: a 400-word explanation. Watch when the first words arrive, and when the last. - Add
max_tokens: 50tofirst.mjs. What isfinish_reasonnow, and how much of the answer do you get? “Good to know” explains why. - Ask the same question to
qwen3-coder-30b, then togemma-4-26b.
All the details are in the documentation.
