🚀 Technical Briefing: This tutorial is part of our deep-dive series on Agentic Workflows at Gate of AI.
For the full technical breakdown, interactive code sandbox, and the native Arabic translation, visit the original article here. "messages": [ { "role": "user", "content": "Explain an API gateway." } ] } The gateway accepts up to 20 messages, permits only , , and roles, and limits each message to 12,000 characters by default.
These are application controls rather than token measurements.
Characters and model tokens are not the same unit, so a character limit should not be presented as a precise cost or usage limit.
The upstream service in this tutorial receives a wrapper containing a request ID and the validated messages.
Its expected successful response is JSON.
The gateway does not transform that JSON into a vendor-neutral completion format because no provider response format is verified in the available context.
Owning a small, documented internal contract is safer than guessing at a provider API. cd nodejs-ai-api-gateway npm init -y npm pkg set type=module npm pkg set scripts.start="node src/server.js" npm pkg set scripts.dev="node --watch src/server.js" npm pkg set engines.node=">=18.0.0" mkdir -p src Create a file before creating local configuration.
Never commit credentials or deployment-specific environment files to source control.
Next, create . is the only required upstream setting.
The URL must point to a service that your deployment can reach and is authorised to use.
The example uses a loopback URL only as a local-development value.
Do not put upstream credentials in browser-exposed environment variables.
If the model service requires credentials, keep them in the gateway’s server-side deployment environment and attach them only on the server.
This tutorial does not include an authorization header because its name, format, and credential lifecycle are not established by the verified context. function readPositiveInteger(name, fallback, minimum, maximum) { const raw = process.env[name] ??
String(fallback); const value = Number.parseInt(raw, 10); if (!Number.isInteger(value) || value < minimum || value > maximum) { throw new Error(); } return value; } function readUrl(name) { const raw = process.env[name]; if (!raw) { throw new Error(); } try { return new URL(raw).toString(); } catch { throw new Error(); } } export const config = Object.freeze({ port: readPositiveInteger("PORT", 3001, 1, 65535), modelServiceUrl: readUrl("MODEL_SERVICE_URL"), allowedOrigin: process.env.ALLOWED_ORIGIN ?? "http://localhost:3000", maxBodyBytes: readPositiveInteger("MAX_BODY_BYTES", 262144, 1024, 1048576), maxMessageChars: readPositiveInteger("MAX_MESSAGE_CHARS", 12000, 1, 100000), maxConversationMessages: readPositiveInteger( "MAX_CONVERSATION_MESSAGES", 20, 1, 100 ), requestsPerMinute: readPositiveInteger( "REQUESTS_PER_MINUTE", 30, 1, 10000 ), upstreamTimeoutMs: readPositiveInteger( "UPSTREAM_TIMEOUT_MS", 30000, 1000, 120000 ) }); For local development, load environment values before starting Node.
One straightforward option is to export the values in your shell.
Deployment platforms should inject server-side environment variables through their own protected configuration mechanism.
The code above intentionally does not rely on an unverified configuration library. import http from "node:http"; import { config } from "./config.js"; const rateWindows = new Map(); function sendJson(response, statusCode, body, requestId) { const payload = JSON.stringify(body); response.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8", "Content-Length": Buffer.byteLength(payload), "X-Request-Id": requestId, "Cache-Control": "no-store" }); response.end(payload); } function getRequestId(request) { const supplied = request.headers["x-request-id"]; if (typeof supplied === "string" && supplied.length > 0 && supplied.length <= 128) { return supplied; } return crypto.randomUUID(); } function applyCors(request, response) { const origin =