In this tutorial we'll build a small Express server with three endpoints: one for chat, one that generates images and one that starts a video generation job. We'll use the official openai npm package for all of it. The only change from a standard OpenAI setup is the base URL, because we'll route everything through ToAPIs, an OpenAI-compatible AI API gateway with text, image, video and audio models behind one API key and unified billing.
By the end you'll have a backend your frontend can call, with a single secret to manage.
Prerequisites
- Node.js 18+ (for native fetch and ES modules)
- A ToAPIs API key. Sign up on the ToAPIs site.
- Basic Express knowledge
1. Project setup
mkdir ai-gateway-demo && cd ai-gateway-demo npm init -y npm pkg set type=module npm install express openai export TOAPIS_API_KEY="your-key-here" 2. A shared client
import OpenAI from "openai"; export const BASE_URL = process.env.TOAPIS_BASE_URL ?? "https://toapis.com/v1"; export const client = new OpenAI({ apiKey: process.env.TOAPIS_API_KEY, baseURL: BASE_URL, }); export const MODELS = { text: process.env.TEXT_MODEL ?? "gpt-5.6-terra", image: process.env.IMAGE_MODEL ?? "gpt-image-2", video: process.env.VIDEO_MODEL ?? "seedance-2", }; 3. Chat endpoint
import express from "express"; import { client, MODELS, BASE_URL } from "./ai.js"; const app = express(); app.use(express.json({ limit: "1mb" })); app.post("/api/chat", async (req, res, next) => { try { const { message } = req.body; if (!message) return res.status(400).json({ error: "message is required" }); const completion = await client.chat.completions.create({ model: MODELS.text, messages: [ { role: "system", content: "You are a concise, helpful assistant." }, { role: "user", content: message }, ], }); res.json({ reply: completion.choices[0].message.content }); } catch (err) { next(err); } }); app.post("/api/image", async (req, res, next) => { try { const { prompt } = req.body; if (!prompt) return res.status(400).json({ error: "prompt is required" }); const result = await client.images.generate({ model: MODELS.image, prompt }); const item = result.data[0]; res.json({ url: item.url ?? null, b64_json: item.b64_json ?? null }); } catch (err) { next(err); } }); app.listen(3000, () => console.log("Listening on http://localhost:3000")); The same account can also call POST /v1/videos/generations with a model such as seedance-2. Docs: https://docs.toapis.com
Why this shape lasts
OpenAI's request schema became the de facto integration contract. If your gateway speaks that contract, existing SDKs, agents, and eval harnesses keep working. You can swap model to try a cheaper or faster option, and billing stays in one place.
Try ToAPIs: https://toapis.com/en · Pricing: https://toapis.com/en/pricing · MCP: https://api.toapis.com/mcp

