homeresume
 
   
🔍

AI image generation with OpenAI API

June 10, 2026

OpenAI exposes image generation through the Image API (POST /images/generations). The official openai npm package wraps it as client.images.generate. This post walks through the main request parameters and how to save generated images from Node.js.

The examples use gpt-image-2, OpenAI's latest GPT Image model. GPT Image models always return base64-encoded image data in data[].b64_json. Use output_format for the on-disk file type and put artistic direction in the prompt.

For text generation with the same package, see the Chat Completions API and Responses API posts. Image generation is also available through Responses API tools, but this post focuses on the dedicated Image API endpoint.

The running scenario: generate marketing hero images for a fictional todo app.

Prerequisites

  • OpenAI account
  • Generated API key
  • Enabled billing
  • Node.js version 26
  • openai package installed (npm i openai)

Client setup

Create a client with your API key (read from the environment in production).

import OpenAI from 'openai';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

The same SDK can target other hosts that implement a compatible API by setting baseURL and apiKey:

const client = new OpenAI({
apiKey: process.env.LLM_API_KEY,
baseURL: 'https://your-gateway.example/v1',
});

Azure OpenAI uses AzureOpenAI instead. Confirm your provider supports the Image API and the model you pass to model.

Basic integration

Call client.images.generate with model and prompt. The examples use gpt-image-2, older snapshots include gpt-image-1.5, gpt-image-1, and gpt-image-1-mini. Pin a snapshot (for example gpt-image-2-2026-04-21) when you need stable behavior across deploys.

The prompt describes what to generate. GPT Image models accept up to about 32,000 characters. Be specific about subject, layout, colors, and style.

GPT Image models always return base64 in data[].b64_json. Decode it and write the file yourself.

import { writeFile } from 'node:fs/promises';
const prompt = `
Minimal flat illustration for a productivity app landing page.
Show a todo dashboard with a checklist, calendar widget, and soft pastel palette.
No text labels on screen elements.
`.trim();
const result = await client.images.generate({
model: 'gpt-image-2',
prompt,
});
await writeFile('hero.png', Buffer.from(result.data[0].b64_json, 'base64'));

n

Use n to generate multiple images in one request (default 1, maximum 10). Loop over result.data to save each image.

import { writeFile } from 'node:fs/promises';
const result = await client.images.generate({
model: 'gpt-image-2',
prompt:
'Minimal flat illustration of a todo app dashboard, variant layout, soft pastel colors',
n: 2,
});
for (const [index, item] of result.data.entries()) {
await writeFile(
`hero-${index}.png`,
Buffer.from(item.b64_json, 'base64'),
);
}

Size

Control dimensions with size. Common presets are 1024x1024 (square), 1536x1024 (landscape), and 1024x1536 (portrait). auto lets the model pick based on the prompt.

gpt-image-2 also accepts custom WIDTHxHEIGHT strings when width and height are multiples of 16, the aspect ratio is between 1:3 and 3:1, and total pixels stay within the documented limits.

const result = await client.images.generate({
model: 'gpt-image-2',
prompt:
'Minimal flat illustration of a todo app dashboard, portrait orientation, soft pastel colors',
size: '1024x1536',
});

Quality

Set rendering quality with quality. Use low for fast drafts and iterations, then medium or high for final assets. Default is auto.

const draft = await client.images.generate({
model: 'gpt-image-2',
prompt:
'Minimal flat illustration of a todo app dashboard, soft pastel colors',
quality: 'low',
});
const final = await client.images.generate({
model: 'gpt-image-2',
prompt:
'Minimal flat illustration of a todo app dashboard, soft pastel colors, polished details',
quality: 'high',
size: '1024x1536',
});

Output format

GPT Image models return base64 in the JSON response. Use output_format to control the encoded file type: png (default), jpeg, or webp.

import { writeFile } from 'node:fs/promises';
const result = await client.images.generate({
model: 'gpt-image-2',
prompt:
'Minimal flat illustration of a todo app dashboard, soft pastel colors',
output_format: 'jpeg',
});
await writeFile('hero.jpg', Buffer.from(result.data[0].b64_json, 'base64'));

Compression

When output_format is jpeg or webp, set output_compression from 0 to 100 to trade file size for quality. JPEG is often faster than PNG when latency matters.

const result = await client.images.generate({
model: 'gpt-image-2',
prompt:
'Minimal flat illustration of a todo app dashboard, soft pastel colors',
output_format: 'webp',
output_compression: 50,
});

Background

Use background: 'transparent' with png or webp on models that support it when you need a cutout asset. gpt-image-2 does not support transparent backgrounds; use gpt-image-1.5 or an earlier GPT Image model for that workflow, or bake the background into the prompt.

const result = await client.images.generate({
model: 'gpt-image-1.5',
prompt: 'Flat icon of a checkmark, no background, centered',
output_format: 'png',
background: 'transparent',
});

Production notes

  • Cost scales with quality and size. See OpenAI pricing before generating at scale.
  • Moderation - use moderation: 'auto' (default) or 'low' on GPT Image models when you need less restrictive filtering.
  • Errors - handle image_generation_user_error (for example moderation_blocked) by changing the prompt or inputs; do not blindly retry.
  • Latency - complex prompts can take up to about two minutes.
  • Storage - decode and persist files yourself. GPT Image responses are base64 in JSON.

Demo

Runnable scripts for each section live in the openai-image-generation-demo folder. Get access via code demos.

LLM integration with OpenRouter

June 8, 2026

OpenRouter is a unified API gateway to hundreds of language models from providers such as OpenAI, Anthropic, Google, and Meta. You use one API key and one billing surface, and swap models by changing a provider/model slug. OpenRouter exposes a Chat Completions-compatible HTTP API.

This post shows three Node.js integration paths: the official @openrouter/sdk, the openai package with baseURL, and the Vercel AI SDK with @openrouter/ai-sdk-provider.

For deeper patterns on each stack, see the Chat Completions API, OpenAI Responses API (OpenAI direct only), and Vercel AI SDK posts.

Prerequisites

  • OpenRouter account
  • API key
  • Credits or billing enabled as needed
  • Node.js version 26
  • Install packages for the path you use:
    • @openrouter/sdk (npm i @openrouter/sdk)
    • openai (npm i openai)
    • ai and @openrouter/ai-sdk-provider (npm i ai @openrouter/ai-sdk-provider)

Configuration

Read credentials from the environment in production.

VariablePurpose
OPENROUTER_API_KEYBearer token from OpenRouter settings
OPENROUTER_MODELDefault model slug, for example openai/gpt-5.5
OPENROUTER_SITE_URLOptional site URL sent as HTTP-Referer for rankings on openrouter.ai
OPENROUTER_SITE_TITLEOptional app name sent as X-OpenRouter-Title

Model IDs use the provider/model format, for example openai/gpt-5.5, anthropic/claude-opus-4.8, or google/gemini-3.1-flash-lite. Browse the full catalog at openrouter.ai/models.

The examples below use openai/gpt-5.5, matching the model in the other LLM posts in this series. Override it with OPENROUTER_MODEL when you want a different model.

@openrouter/sdk

OpenRouter's official TypeScript SDK is type-safe and generated from the OpenAPI spec.

Client setup

import { OpenRouter } from '@openrouter/sdk';
const client = new OpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
httpReferer: process.env.OPENROUTER_SITE_URL,
appTitle: process.env.OPENROUTER_SITE_TITLE,
});

Basic integration

const response = await client.chat.send({
chatRequest: {
model: process.env.OPENROUTER_MODEL ?? 'openai/gpt-5.5',
messages: [
{ role: 'user', content: 'Write a one-sentence bedtime story about a unicorn.' },
],
},
});
console.log(response.choices[0].message.content);

System prompt

Add a system message before the user turn to set tone, format, and role.

const response = await client.chat.send({
chatRequest: {
model: process.env.OPENROUTER_MODEL ?? 'openai/gpt-5.5',
messages: [
{ role: 'system', content: 'Reply in one short sentence. Use plain language.' },
{ role: 'user', content: 'Explain what an LLM is.' },
],
},
});
console.log(response.choices[0].message.content);

Streaming

Set stream: true and read incremental text from choices[0].delta.content.

const stream = await client.chat.send({
chatRequest: {
model: process.env.OPENROUTER_MODEL ?? 'openai/gpt-5.5',
messages: [{ role: 'user', content: 'List three colors.' }],
stream: true,
},
});
process.stdout.write('[stream] ');
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) {
process.stdout.write(delta);
}
}
process.stdout.write('\n');

Model switching

Change only the model string to route the same code to a different provider.

const models = ['openai/gpt-5.5', 'google/gemini-3.1-flash-lite'];
for (const model of models) {
const response = await client.chat.send({
chatRequest: {
model,
messages: [{ role: 'user', content: 'Reply with exactly one word: ok.' }],
},
});
console.log(model, '->', response.choices[0].message.content);
}

openai package

If you already use the OpenAI SDK, point it at OpenRouter with baseURL. The request shape matches the Chat Completions API.

Client setup

import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.OPENROUTER_API_KEY,
baseURL: 'https://openrouter.ai/api/v1',
defaultHeaders: {
'HTTP-Referer': process.env.OPENROUTER_SITE_URL,
'X-OpenRouter-Title': process.env.OPENROUTER_SITE_TITLE,
},
});

Basic integration

const completion = await client.chat.completions.create({
model: process.env.OPENROUTER_MODEL ?? 'openai/gpt-5.5',
messages: [
{ role: 'user', content: 'Write a one-sentence bedtime story about a unicorn.' },
],
});
console.log(completion.choices[0].message.content);

System prompt

const completion = await client.chat.completions.create({
model: process.env.OPENROUTER_MODEL ?? 'openai/gpt-5.5',
messages: [
{ role: 'system', content: 'Reply in one short sentence. Use plain language.' },
{ role: 'user', content: 'Explain what an LLM is.' },
],
});
console.log(completion.choices[0].message.content);

Streaming

const stream = await client.chat.completions.create({
model: process.env.OPENROUTER_MODEL ?? 'openai/gpt-5.5',
messages: [{ role: 'user', content: 'List three colors.' }],
stream: true,
});
process.stdout.write('[stream] ');
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) {
process.stdout.write(delta);
}
}
process.stdout.write('\n');

For JSON schema output, Markdown-to-HTML, and few-shot prompting, reuse the patterns from the Chat Completions post with the OpenRouter client and model slug above.

Vercel AI SDK

The @openrouter/ai-sdk-provider package exposes OpenRouter models to generateText, streamText, and related helpers from the ai package. See the OpenRouter Vercel AI SDK guide for the full integration reference.

Client setup

import { createOpenRouter } from '@openrouter/ai-sdk-provider';
const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
appUrl: process.env.OPENROUTER_SITE_URL,
appName: process.env.OPENROUTER_SITE_TITLE,
});

The returned provider is callable. Pass a model slug directly: openrouter('openai/gpt-5.5').

Basic integration

import { generateText } from 'ai';
const { text } = await generateText({
model: openrouter(process.env.OPENROUTER_MODEL ?? 'openai/gpt-5.5'),
prompt: 'Write a one-sentence bedtime story about a unicorn.',
});
console.log(text);

System prompt

const { text } = await generateText({
model: openrouter(process.env.OPENROUTER_MODEL ?? 'openai/gpt-5.5'),
system: 'Reply in one short sentence. Use plain language.',
prompt: 'Explain what an LLM is.',
});
console.log(text);

Streaming

import { streamText } from 'ai';
const result = streamText({
model: openrouter(process.env.OPENROUTER_MODEL ?? 'openai/gpt-5.5'),
prompt: 'List three colors.',
});
process.stdout.write('[stream] ');
for await (const part of result.textStream) {
process.stdout.write(part);
}
process.stdout.write('\n');

For structured output, embeddings, and web search, see the Vercel AI SDK post. Those patterns apply when you call OpenAI directly; OpenRouter coverage depends on the model and endpoint.

Demo

Runnable scripts for each integration path live in the openrouter-demo folder. Get access via code demos.

Supabase basics with Node.js

June 3, 2026

Supabase is an open-source backend platform built around managed PostgreSQL. You get a database, auto-generated REST APIs (via PostgREST), Auth, file Storage, Realtime subscriptions, and Edge Functions - with a dashboard and SQL editor on top.

Compared to running Postgres yourself, Supabase adds hosted infra, API layers, and product features without you wiring them up. Compared to an ORM-only stack, you often talk to Postgres through the Supabase client or SQL, with RLS (row level security) enforcing access at the database layer.

Prerequisites

  • Supabase account (free tier is enough for learning)
  • Node.js version 26
  • @supabase/supabase-js installed (npm i @supabase/supabase-js)

Create a project and database

  1. In the Supabase dashboard, choose New project, pick a region, and set the database password.
  2. Wait until the project is ready.
  3. Open SQL Editor and run the schema below.

The Table Editor is fine for quick experiments; SQL Editor keeps schema reproducible in git and reviews.

create table if not exists public.todos (
id bigint generated always as identity primary key,
title text not null,
done boolean not null default false,
created_at timestamptz not null default now()
);
create or replace function public.list_open_todos()
returns setof public.todos
language sql
security definer
set search_path = public
as $$
select * from public.todos where done = false order by id;
$$;
create or replace function public.mark_todo_done(todo_id bigint)
returns setof public.todos
language sql
security definer
set search_path = public
as $$
update public.todos set done = true where id = todo_id returning *;
$$;
grant execute on function public.list_open_todos() to service_role;
grant execute on function public.mark_todo_done(bigint) to service_role;

This schema skips RLS setup because this post uses the secret key from Node.js, which bypasses RLS. For browser or mobile clients, use the publishable key instead - it obeys Row Level Security when you enable it on tables.

list_open_todos is a Postgres function exposed as an RPC endpoint.

Postgres functions default to security invoker: they run with the caller's permissions, so RLS applies as that role. security definer runs the function as its owner (often a privileged database role), not as the caller.

API keys and connection

On the project Overview tab you will find:

  • Project URL (https://<ref>.supabase.co) - for SUPABASE_URL
  • Publishable key (sb_publishable_..., legacy anon) - for browsers and mobile apps where RLS should limit access; this post does not use it

For Node.js backends, use a secret key from Project SettingsAPI Keys:

  • Secret key(s) (sb_secret_...) - full data access, bypasses RLS; trusted servers only, never in client bundles or public repos. Replaces the legacy service_role key.
  • Reveal or create a secret key in the dashboard (legacy projects: Legacy anon, service_role API keys tab → service_role).

The Supabase JS client talks to PostgREST and can query tables (from) and call registered functions (rpc) only after they exist. It does not run DDL (create table, create function, grant, and similar), so apply the schema in the SQL Editor (or via Supabase CLI migrations in larger projects).

Store values in environment variables:

SUPABASE_URL=https://<ref>.supabase.co
SUPABASE_SECRET_KEY=your-secret-key

Client setup

Create a client with the project URL and secret key. Read values from the environment in real apps; never commit the secret key.

import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SECRET_KEY
);

Every table you create in the public schema is available as supabase.from('<table>'). Custom SQL functions are called with supabase.rpc('<function_name>').

Insert data

Insert one or more rows and return the created records with .select().

const { data, error } = await supabase
.from('todos')
.insert([
{ title: 'Learn Supabase client', done: false },
{ title: 'Run RPC example', done: false },
{ title: 'Ship demo', done: true },
])
.select();
if (error) {
throw new Error(error.message);
}
console.log(data);

Read, update, and delete

Select with filters and ordering:

const { data, error } = await supabase
.from('todos')
.select('*')
.eq('done', false)
.order('id');

Update matching rows:

const { data, error } = await supabase
.from('todos')
.update({ done: true })
.eq('id', 1)
.select();

Delete matching rows:

const { error } = await supabase
.from('todos')
.delete()
.eq('id', 1);

Chain .eq(), .in(), .limit(), and other filter helpers the same way across operations.

RPC

Call a database function by name. Our list_open_todos() returns open todos without repeating filter logic in the app.

const { data, error } = await supabase.rpc('list_open_todos');
if (error) {
throw new Error(error.message);
}
console.log(data);

Functions with parameters map to a second argument:

await supabase.rpc('mark_todo_done', { todo_id: 42 });

Define parameters in SQL (todo_id bigint) and grant execute to service_role when calling RPC from a secret-key backend.

What else matters

  • Row Level Security (RLS) - When enabled on a table, Postgres denies access until policies allow it. The publishable key is subject to RLS; the secret key is not. Add policies per role (anon, authenticated) and operation when you turn RLS on.
  • Auth - Supabase Auth stores users in auth.users. After sign-in, the client JWT includes the authenticated role so policies can use auth.uid() for per-user rows.
  • Migrations - Avoid changing production only via the dashboard. Track SQL in versioned migration files and apply with the Supabase CLI (supabase db push, linked projects) so environments stay aligned.
  • Generated types - Run supabase gen types typescript --project-id <ref> to emit TypeScript types from your schema for a type-safe client.
  • Storage - S3-compatible buckets for files (images, PDFs) with bucket policies analogous to RLS.
  • Realtime - Subscribe to insert/update/delete on tables or channels for live UI updates.
  • Edge Functions - Deno functions for webhooks, light API logic, or tasks that should not live in the database.
  • Local dev - supabase init and supabase start spin up local Postgres and services; useful for offline work, while cloud projects are fastest for a first tutorial.

Demo

Runnable scripts for this post live in the supabase-basics-demo folder in the private demos repository. Get access via code demos.

Integration with Hugging Face Inference API

June 1, 2026

Hugging Face hosts thousands of open models for NLP, vision, and other tasks. The Inference API (via Inference Providers) lets you call those models over HTTP. The @huggingface/inference package from huggingface.js is the Node.js client.

Prerequisites

  • Hugging Face account
  • Access token with permission to call Inference (create a token of type Read or Fine-grained with inference access)
  • Node.js version 26
  • @huggingface/inference installed (npm i @huggingface/inference)

Some models (especially image generation) are routed through Inference Providers. Enable providers and billing in account settings when a model requires it.

Client setup

Pass your token to InferenceClient. Read it from the environment in production.

import { InferenceClient } from '@huggingface/inference';
const client = new InferenceClient(process.env.HF_TOKEN);

Summarization

Shrink longer text into a short summary. Pick a model trained for summarization and respect its max input length.

const result = await client.summarization({
model: 'facebook/bart-large-cnn',
inputs: `The tower is 324 metres (1,063 ft) tall, about the same height as an 81-storey building, and the tallest structure in Paris. Its base is square, measuring 125 metres (410 ft) on each side. During its construction, the Eiffel Tower surpassed the Washington Monument to become the tallest man-made structure in the world, a title it held for 41 years until the Chrysler Building in New York City was finished in 1930. It was initially criticized by some artists and intellectuals, but it became a global cultural icon of France and one of the most recognizable structures in the world.`,
parameters: {
max_length: 80,
},
});
console.log(result.summary_text);

Text classification

Assign labels with confidence scores. Common use case: sentiment analysis on short text.

const labels = await client.textClassification({
model: 'distilbert-base-uncased-finetuned-sst-2-english',
inputs: 'I really enjoyed this workshop.',
});
for (const { label, score } of labels) {
console.log(label, score.toFixed(4));
}

Text to image

Generate an image from a text prompt. The client returns a Blob; write it to disk or serve it from your app.

import { writeFileSync } from 'node:fs';
const image = await client.textToImage({
provider: 'hf-inference',
model: 'black-forest-labs/FLUX.1-schnell',
inputs: 'A watercolor painting of a fox in an autumn forest',
parameters: {
num_inference_steps: 4,
},
});
const buffer = Buffer.from(await image.arrayBuffer());
writeFileSync('output.png', buffer);

With provider: 'hf-inference', use a model from the hf-inference catalog for that task (for example black-forest-labs/FLUX.1-schnell). Older Hub repos such as stabilityai/stable-diffusion-2-1 may no longer exist or lack provider mapping.

Image models are often slower and may incur provider usage charges.

Demo

Runnable scripts for each task live in the huggingface-inference-api-demo folder. Get access via code demos.

LLM integration with OpenAI Responses API

May 31, 2026

Large language models (LLMs) understand and generate text from prompts. OpenAI exposes models through the Responses API. The official openai npm package is the practical way to call it from Node.js. This post covers common patterns beyond a single prompt string.

For the Chat Completions API (messages, choices[0].message.content), see the dedicated post. For the same patterns with the Vercel AI SDK (generateText, streamText), see the dedicated post.

Prerequisites

  • OpenAI account
  • Generated API key
  • Enabled billing
  • Node.js version 26
  • openai package installed (npm i openai)
  • For Markdown output: marked, dompurify, and jsdom (npm i marked dompurify jsdom)

Client setup

Create a client with your API key (read from the environment in production).

import OpenAI from 'openai';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

The same SDK can target other hosts that implement a compatible API by setting baseURL and apiKey:

const client = new OpenAI({
apiKey: process.env.LLM_API_KEY,
baseURL: 'https://your-gateway.example/v1',
});

Azure OpenAI uses AzureOpenAI instead. Many third-party gateways support Chat Completions only; the examples below use client.responses.*, so confirm your provider supports the Responses API (especially for tools like web search).

Basic integration

Pass a string as input and read output_text from the response.

const response = await client.responses.create({
model: 'gpt-5.5',
input: 'Write a one-sentence bedtime story about a unicorn.',
});
console.log(response.output_text);

System prompt

Use top-level instructions for stable behavior (tone, format, role). They take precedence over casual wording in the user message.

const response = await client.responses.create({
model: 'gpt-5.5',
instructions: 'Reply in one short sentence. Use plain language.',
input: 'Explain what an LLM is.',
});
console.log(response.output_text);

Few-shot prompting

Pass prior turns as an input array with user and assistant roles, then the new user message. Keep task rules in instructions.

const response = await client.responses.create({
model: 'gpt-5.5',
instructions:
'Classify sentiment as exactly one word: positive, negative, or neutral.',
input: [
{ role: 'user', content: 'I love this!' },
{ role: 'assistant', content: 'positive' },
{ role: 'user', content: 'This is awful.' },
{ role: 'assistant', content: 'negative' },
{ role: 'user', content: 'It is fine I guess.' },
],
});
console.log(response.output_text);

Streaming

Set stream: true and handle response.output_text.delta events for incremental text.

const stream = await client.responses.create({
model: 'gpt-5.5',
input: 'List three colors.',
stream: true,
});
for await (const event of stream) {
if (event.type === 'response.output_text.delta') {
process.stdout.write(event.delta);
}
}

Structured output with JSON schema

Constrain the model to JSON matching your schema via text.format. With strict: true, the output should match the schema.

const response = await client.responses.create({
model: 'gpt-5.5',
input: 'The film Inception was directed by Christopher Nolan.',
text: {
format: {
type: 'json_schema',
name: 'movie_summary',
strict: true,
schema: {
type: 'object',
properties: {
title: { type: 'string' },
director: { type: 'string' },
},
required: ['title', 'director'],
additionalProperties: false,
},
},
},
});
const data = JSON.parse(response.output_text);
console.log(data.title, data.director);

For typed parsing with Zod, you can use client.responses.parse() instead of JSON.parse.

Markdown output to HTML

Ask for Markdown in instructions, then convert output_text to HTML and sanitize before rendering (for example with innerHTML in the browser or when storing HTML).

import { marked } from 'marked';
import { JSDOM } from 'jsdom';
import DOMPurify from 'dompurify';
const purify = DOMPurify(new JSDOM('').window);
const response = await client.responses.create({
model: 'gpt-5.5',
instructions: 'Reply in Markdown only. Use a heading and a short bullet list.',
input: 'Explain what an LLM is in three bullet points.',
});
const markdown = response.output_text;
const html = marked.parse(markdown);
const safeHtml = purify.sanitize(html);

Always run DOMPurify.sanitize on model-generated HTML. The model can emit unsafe markup; sanitization strips scripts and other dangerous content.

Web search tool

Enable the built-in web search tool when the answer should use current information from the web.

const response = await client.responses.create({
model: 'gpt-5.5',
tools: [{ type: 'web_search' }],
include: ['web_search_call.action.sources'],
input: 'What was a major tech headline this week? Cite sources briefly.',
});
console.log(response.output_text);

Web search adds latency and tool usage cost. Use a model that supports tools.

Demo

Runnable scripts for each section live in the openai-responses-api-demo folder. Get access via code demos.

2024

HTTP timeout with Axios

September 26, 2024

Setting up a timeout for HTTP requests can prevent the connection from hanging forever, waiting for the response. It can be set on the client side to improve user experience, and on the server side to improve inter-service communication.

axios package provides a timeout parameter for this functionality.

const HTTP_TIMEOUT = 3000;
const URL = 'https://www.google.com:81';
(async () => {
try {
await axios(URL, {
timeout: HTTP_TIMEOUT
});
} catch (error) {
console.error('Request timed out', error.cause);
}
})();

Use this snippet also to simulate aborted requests.

Demo

Runnable code for this post lives in the axios-http-demo folder. Get access via code demos.

2023

Integration with GitHub GraphQL API

December 22, 2023

GitHub provides GraphQL API to create integrations, retrieve data, and automate workflows.

Prerequisites

  • GitHub token (Settings Developer Settings Personal access tokens)

Integration

Below is an example of retrieving sponsorable users by location.

export async function getUsersBy(location) {
return fetch('https://api.github.com/graphql', {
method: 'POST',
body: JSON.stringify({
query: `query {
search(type: USER, query: "location:${location} is:sponsorable", first: 100) {
edges {
node {
... on User {
bio
login
viewerCanSponsor
}
}
}
userCount
}
}`
}),
headers: {
ContentType: 'application/json',
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
}
})
.then((response) => response.json())
.then((response) => response.data?.search?.edges || []);
}

Demo

Runnable Node.js script for this post lives in the github-graphql-api-nodejs-demo folder. Get access via code demos.

License key verification with Gumroad API

November 16, 2023

Gumroad allows verifying license keys via API calls to limit the usage of the keys. It can be helpful to prevent the redistribution of products like desktop apps.

Allow generating a unique license key per sale in product settings, and the product ID will be shown there. Below is the code snippet for verification.

try {
const requestBody = new URLSearchParams();
requestBody.append('product_id', process.env.PRODUCT_ID);
requestBody.append('license_key', process.env.LICENSE_KEY);
requestBody.append('increment_uses_count', true);
const response = await fetch('https://api.gumroad.com/v2/licenses/verify', {
method: 'POST',
body: requestBody
});
const data = await response.json();
if (data.purchase?.test) {
console.log('Skipping verification for test purchase');
return;
}
const verificationLimit = Number(process.env.VERIFICATION_LIMIT);
if (data.uses >= verificationLimit + 1) {
throw new Error('Verification limit exceeded');
}
if (!data.success) {
throw new Error(data.message);
}
} catch (error) {
if (error?.response?.status === 404) {
console.log("License key doesn't exist");
return;
}
console.log('Verifying license key failed', error);
}

Demo

Runnable code for this post lives in the license-key-verification-gumroad-api-demo folder. Get access via code demos.

Integration with Notion API

September 9, 2023

Notion is a versatile workspace tool combining note-taking, task management, databases, and collaboration features into a single platform.

It also supports integration with Notion content, facilitating tasks such as creating pages, retrieving a block, and filtering database entries via API.

Prerequisites

  • Notion account
  • Generated Integration token (Settings & Members Connections Develop or manage integrations New integration)
  • Notion database ID (open database as full page, extract database ID from the URL (https://notion.so/<USERNAME>/<DATABASE_ID>?v=v))
  • Added Notion connection (three dots (...) menu Add Connections choose created integration)
  • @notionhq/client package installed

Integration

Below is an example of interacting with Notion API to create the page (within the chosen database) with icon, cover, properties, and child blocks.

const { Client } = require('@notionhq/client');
const notion = new Client({ auth: process.env.NOTION_INTEGRATION_TOKEN });
const response = await notion.pages.create({
parent: {
type: 'database_id',
database_id: process.env.NOTION_DATABASE_ID
},
icon: {
type: 'emoji',
emoji: '🆗'
},
cover: {
type: 'external',
external: {
url: 'https://cover.com'
}
},
properties: {
Name: {
title: [
{
type: 'text',
text: {
content: 'Some name'
}
}
]
},
Score: {
number: 42
},
Tags: {
multi_select: [
{
name: 'A'
},
{
name: 'B'
}
]
},
Generation: {
select: {
name: 'I'
}
}
// other properties
},
children: [
{
object: 'block',
type: 'bookmark',
bookmark: {
url: 'https://bookmark.com'
}
}
]
});

Async API documentation 101

May 21, 2023

Async API documentation is used for documenting events in event-driven systems, like Kafka events. All of the event DTOs are stored in one place. It supports YAML and JSON formats.

It contains information about channels and components. Channels and components are defined with their messages and DTO schemas, respectively.

{
"asyncapi": "2.6.0",
"info": {
"title": "Events docs",
"version": "1.0.0"
},
"channels": {
"topic_name": {
"publish": {
"message": {
"schemaFormat": "application/vnd.oai.openapi;version=3.0.0",
"payload": {
"type": "object",
"properties": {
"counter": {
"type": "number"
}
},
"required": ["counter"]
}
}
}
}
},
"components": {
"schemas": {
"EventDto": {
"type": "object",
"properties": {
"counter": {
"type": "number"
}
},
"required": ["counter"]
}
}
}
}

Autogeneration

Async API docs can be autogenerated by following multiple steps:

  • define DTOs and their required and optional fields with ApiProperty and ApiPropertyOptional decorators (from the @nestjs/swagger package), respectively
  • generate OpenAPI docs from the defined DTOs
  • parse and reuse component schemas from generated OpenAPI documentation to build channel messages and component schemas for Async API docs

Validation

Use AsyncAPI Studio to validate the written specification.

Preview

There are multiple options

  • AsyncAPI Studio

  • VSCode extension asyncapi-preview, open the command palette, and run the Preview AsyncAPI command.

UI generation

  • Install @asyncapi/cli and corresponding template package (e.g., @asyncapi/html-template, @asyncapi/markdown-template)
  • Update package.json with scripts
{
"scripts": {
// ...
"generate-docs:html": "asyncapi generate fromTemplate ./asyncapi/asyncapi.json @asyncapi/html-template --output ./docs/html",
"generate-docs:markdown": "asyncapi generate fromTemplate ./asyncapi/asyncapi.json @asyncapi/markdown-template --output ./docs/markdown"
}
}

LLM integration with OpenAI Completions API

March 19, 2023

ChatGPT is a large language model (LLM) that understands and processes human prompts to produce helpful responses. OpenAI exposes models through the Chat Completions API. The official openai npm package is the practical way to call it from Node.js. This post covers common patterns beyond a single user message.

For the newer Responses API (web search, instructions, and input), see the dedicated post.

Prerequisites

  • OpenAI account
  • Generated API key
  • Enabled billing
  • Node.js version 26
  • openai package installed (npm i openai)
  • For Markdown output: marked, dompurify, and jsdom (npm i marked dompurify jsdom)

Client setup

Create a client with your API key (read from the environment in production).

import OpenAI from 'openai';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

For OpenRouter specifically, see the OpenRouter integration post. The same SDK can target OpenAI-compatible gateways by setting baseURL and apiKey:

const client = new OpenAI({
apiKey: process.env.LLM_API_KEY,
baseURL: 'https://your-gateway.example/v1',
});

Azure OpenAI uses AzureOpenAI instead. Many third-party hosts implement Chat Completions, so this API is a common integration path.

Basic integration

Send a user message and read the assistant reply from choices[0].message.content.

const completion = await client.chat.completions.create({
model: 'gpt-5.5',
messages: [
{ role: 'user', content: 'Write a one-sentence bedtime story about a unicorn.' },
],
});
console.log(completion.choices[0].message.content);

System prompt

Add a system message (or developer on newer models) before the user message to set tone, format, and role.

const completion = await client.chat.completions.create({
model: 'gpt-5.5',
messages: [
{ role: 'system', content: 'Reply in one short sentence. Use plain language.' },
{ role: 'user', content: 'Explain what an LLM is.' },
],
});
console.log(completion.choices[0].message.content);

Few-shot prompting

Include prior user and assistant turns in messages, then the new user question. Keep task rules in the system message.

const completion = await client.chat.completions.create({
model: 'gpt-5.5',
messages: [
{
role: 'system',
content:
'Classify sentiment as exactly one word: positive, negative, or neutral.',
},
{ role: 'user', content: 'I love this!' },
{ role: 'assistant', content: 'positive' },
{ role: 'user', content: 'This is awful.' },
{ role: 'assistant', content: 'negative' },
{ role: 'user', content: 'It is fine I guess.' },
],
});
console.log(completion.choices[0].message.content);

Streaming

Set stream: true and read incremental text from choices[0].delta.content.

const stream = await client.chat.completions.create({
model: 'gpt-5.5',
messages: [{ role: 'user', content: 'List three colors.' }],
stream: true,
});
process.stdout.write('[stream] ');
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) {
process.stdout.write(delta);
}
}
process.stdout.write('\n');

Structured output with JSON schema

Use response_format with type: 'json_schema' and strict: true so the model returns JSON matching your schema.

const completion = await client.chat.completions.create({
model: 'gpt-5.5',
messages: [
{
role: 'user',
content: 'The film Inception was directed by Christopher Nolan.',
},
],
response_format: {
type: 'json_schema',
json_schema: {
name: 'movie_summary',
strict: true,
schema: {
type: 'object',
properties: {
title: { type: 'string' },
director: { type: 'string' },
},
required: ['title', 'director'],
additionalProperties: false,
},
},
},
});
const data = JSON.parse(completion.choices[0].message.content);
console.log(data.title, data.director);

For typed parsing with Zod, you can use client.chat.completions.parse() instead of JSON.parse.

Markdown output to HTML

Ask for Markdown in the system message, then convert the assistant reply to HTML and sanitize before rendering.

import { marked } from 'marked';
import { JSDOM } from 'jsdom';
import DOMPurify from 'dompurify';
const purify = DOMPurify(new JSDOM('').window);
const completion = await client.chat.completions.create({
model: 'gpt-5.5',
messages: [
{
role: 'system',
content: 'Reply in Markdown only. Use a heading and a short bullet list.',
},
{ role: 'user', content: 'Explain what an LLM is in three bullet points.' },
],
});
const markdown = completion.choices[0].message.content;
const html = marked.parse(markdown);
const safeHtml = purify.sanitize(html);

Always run DOMPurify.sanitize on model-generated HTML. The model can emit unsafe markup; sanitization strips scripts and other dangerous content.

Demo

Runnable scripts for each section live in the openai-chat-completions-api-demo folder. Get access via code demos.

Documenting REST APIs with OpenAPI specs (NestJS/Swagger)

March 16, 2023

OpenAPI is a language-agnostic specification for declaring API documentation for REST APIs. It contains the following information:

  • API information like title, description, version
  • endpoints definitions with request and response parameters
  • DTOs and security schemas
openapi: 3.0.0
paths:
/users:
post:
operationId: UsersController_createUser
summary: Create user
description: Create a new user
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserDto'
responses:
'201':
description: 'User is created'
info:
title: nestjs-starter
description: Minimal NestJS boilerplate
version: 0.1.0
contact: {}
tags: []
servers: []
components:
securitySchemes:
token:
type: apiKey
scheme: api_key
in: header
name: auth-token
schemas:
CreateUserDto:
type: object
properties:
firstName:
type: string
example: tester
description: first name of the user
required:
- firstName

NestJS provides a Swagger plugin for generating the API docs.

Setup

Configure API documentation with the specified endpoint, like /api-docs, which shows the generated docs.

const SWAGGER_API_ENDPOINT = '/api-docs';
// ...
export const setupApiDocs = (app: INestApplication): void => {
const options = new DocumentBuilder()
.setTitle(SWAGGER_API_TITLE)
.setDescription(SWAGGER_API_DESCRIPTION)
.setVersion(SWAGGER_API_VERSION)
.addSecurity('token', {
type: 'apiKey',
scheme: 'api_key',
in: 'header',
name: 'auth-token'
})
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup(SWAGGER_API_ENDPOINT, app, document);
};

Configure the plugin in the NestJS config file.

{
"compilerOptions": {
"plugins": ["@nestjs/swagger"]
}
}

JSON and YAML formats are generated at /api-docs-json and /api-docs-yaml endpoints, respectively.

Decorators

  • ApiTags groups endpoints
@ApiTags('users')
@Controller('users')
export class UsersController {
// ...
}
  • ApiOperation provides more details like a summary and description of the endpoint
@ApiOperation({
summary: 'Get user',
description: 'Get user by id',
})
@Get(':id')
async getById(
@Param('id', new ParseUUIDPipe()) id: string,
): Promise<UserDto> {
// ...
}
  • ApiOperation can be used to mark an endpoint as deprecated
@ApiOperation({ deprecated: true })
  • @ApiProperty and @ApiPropertyOptional should be used for request and response DTOs fields. Example and description values will be shown in the generated documentation.
export class CreateUserDto {
@ApiProperty({ example: 'John', description: 'first name of the user' })
// ...
public firstName: string;
@ApiPropertyOptional({ example: 'Doe', description: 'last name of the user' })
// ...
public lastName?: string;
}
  • ApiHeader documents endpoint headers
@ApiHeader({
name: 'correlation-id',
required: false,
description: 'unique id for correlated logs',
example: '7ea2c7f7-8b46-475d-86f8-7aaaa9e4a35b',
})
@Get()
getHello(): string {
// ...
}
  • ApiResponse specifies which responses are expected, like error responses. NestJS' Swagger package provides decorators for specific status codes like ApiBadRequestResponse.
// ...
@ApiResponse({ type: NotFoundException, status: HttpStatus.NOT_FOUND })
@ApiBadRequestResponse({ type: BadRequestException })
@Get(':id')
async getById(
@Param('id', new ParseUUIDPipe()) id: string,
): Promise<UserDto> {
return this.userService.findById(id);
}
// ...
  • ApiSecurity('token') uses a custom-defined security strategy, token in this case. Other options are to use already defined strategies like ApiBearerAuth.
@ApiSecurity('token')
@Controller()
export class AppController {
// ...
}
// ...
@ApiBearerAuth()
@Controller()
export class AppController {
// ...
}
  • ApiExcludeEndpoint and ApiExcludeController exclude one endpoint and the whole controller, respectively.
export class AppController {
@ApiExcludeEndpoint()
@Get()
getHello(): string {
// ...
}
}
// ...
@ApiExcludeController()
@Controller()
export class AppController {
// ...
}
  • ApiBody with ApiExtraModels add an example for the request body
const CreateUserDtoExample = {
firstName: 'Tester',
};
@ApiExtraModels(CreateUserDto)
@ApiBody({
schema: {
oneOf: refs(CreateUserDto),
example: CreateUserDtoExample,
},
})
@Post()
async createUser(@Body() newUser: CreateUserDto): Promise<UserDto> {
// ...
}

Importing API to Postman

Import JSON version of API docs as Postman API with Import Link option (e.g., URL http://localhost:8081/api-docs-json). Imported API collection will be available in the APIs tab.

Demo

Runnable NestJS app for this post lives in the swagger-openapi-docs-nestjs-demo folder. Get access via code demos.

2022

Timeout with Fetch API

November 2, 2022

Setting up a timeout for HTTP requests can prevent the connection from hanging forever, waiting for the response. It can be set on the client side to improve user experience, and on the server side to improve inter-service communication. Fetch API is fully available in Node as well from version 18.

AbortController can be utilized to set up timeouts. Instantiated abort controller has a signal property which represents reference to its associated AbortSignal object. Abort signal object is used as a signal parameter in the request with Fetch API, so HTTP request is aborted when abort method is called.

const HTTP_TIMEOUT = 3000;
const URL = 'https://www.google.com:81';
(async () => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), HTTP_TIMEOUT);
try {
const response = await fetch(URL, {
signal: controller.signal
}).then((res) => res.json());
console.log(response);
} catch (error) {
console.error(error);
} finally {
clearTimeout(timeoutId);
}
})();

Use this snippet also to simulate aborted requests.

Demo

Runnable scripts for each timeout approach live in the fetch-api-http-demo folder. Get access via code demos.