homeresume
 
   
🔍

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.

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 || []);
}

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);
}

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"
}
}

Integration with ChatGPT 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.

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.