Building an LLM API Gateway in Node.js An engineer demonstrates how to build an LLM API gateway in Node.js using LiteLLM, addressing the governance problem of direct API calls to providers like OpenAI and Anthropic. The gateway centralizes routing, cost tracking, and rate limiting, with options including LiteLLM, Portkey, and Cloudflare AI Gateway. The walkthrough uses LiteLLM as a proxy with an OpenAI-compatible API, requiring minimal changes to existing Node.js code. Your LLM bill just tripled and nobody on the team can say why. Sound familiar? If you're calling OpenAI or Anthropic directly from every service in your stack, you don't have an integration problem, you have a governance problem. This is where an LLM API gateway earns its keep, and setting one up in Node.js takes less time than debugging your next surprise invoice. Here's what actually happens without a gateway. Every service that calls an LLM hardcodes its own API key, its own retry logic, its own model name. Then one day you want to switch from GPT to Claude for cost reasons, and you're grepping through six repos. Or a junior dev's script goes into an infinite retry loop against a rate limited endpoint and your bill spikes overnight. Or, worse, you have zero visibility into which team or feature is actually driving spend. A gateway sits between your app code and the model providers. Every request flows through one chokepoint, which means you get centralized routing, centralized cost tracking, centralized rate limiting, and a single place to swap providers without touching application code. Enterprise model API spend has already climbed past 8.4 billion dollars and keeps climbing, and most of that spend is going through zero centralized control. That's not a scale problem you'll deal with later, it's a scale problem you're already in. The gotcha nobody warns you about: adding a gateway late in a project is way more painful than adding it on day one, because by then every service has its own bespoke calling convention baked in. If you're greenfield, wire this up before you write your first prompt call. You've got three solid options here and picking the wrong one for your situation is an easy footgun. | Gateway | Best for | Deployment | Standout feature | |---|---|---|---| | LiteLLM | Teams who want full control, self hosted | Your own infra Docker, VM, k8s | Massive provider support, drop in OpenAI compatible API | | Portkey | Teams who want guardrails without building them | Cloud or self hosted | Fully open source under Apache 2.0, over 1600 models across 250 plus providers, 40 plus prebuilt guardrails | | Cloudflare AI Gateway | Teams already on Cloudflare's edge | Cloudflare's network | Near zero added latency since it's already at the edge, built in analytics | If you're the kind of team that wants to own every knob and doesn't mind running another service, reach for LiteLLM. If you want guardrails PII detection, prompt injection checks, content moderation without writing that logic yourself, Portkey's prebuilt set is hard to beat, especially now that it's fully open source. If your traffic already runs through Cloudflare, the AI Gateway is the path of least resistance since there's no new infra to stand up. For this walkthrough I'm going with LiteLLM, since it gives you the clearest picture of what's actually happening under the hood, and that understanding transfers even if you end up choosing Portkey or Cloudflare later. LiteLLM runs as a proxy server with an OpenAI compatible API, so your Node code barely changes, you just point it at a different base URL. litellm-config.yaml model list: - model name: gpt-4o litellm params: model: openai/gpt-4o api key: os.environ/OPENAI API KEY - model name: claude-sonnet litellm params: model: anthropic/claude-sonnet-4-5 api key: os.environ/ANTHROPIC API KEY router settings: routing strategy: least-busy fallbacks: - gpt-4o: claude-sonnet general settings: master key: os.environ/LITELLM MASTER KEY spin up the proxy locally Docker keeps this reproducible across environments docker run -d \ -v $ pwd /litellm-config.yaml:/app/config.yaml \ -e OPENAI API KEY=$OPENAI API KEY \ -e ANTHROPIC API KEY=$ANTHROPIC API KEY \ -e LITELLM MASTER KEY=$LITELLM MASTER KEY \ -p 4000:4000 \ ghcr.io/berriai/litellm:main-latest \ --config /app/config.yaml Now your Node.js code calls the proxy instead of the provider directly. If you're already using the OpenAI SDK, this is a one line change to the base URL: python // lib/llm-client.ts import OpenAI from "openai"; const gateway = new OpenAI { apiKey: process.env.LITELLM MASTER KEY, baseURL: "http://localhost:4000/v1", } ; export async function askModel prompt: string { const response = await gateway.chat.completions.create { model: "gpt-4o", messages: { role: "user", content: prompt } , } ; return response.choices 0 .message.content; } That's it. Your application code has no idea it's talking to a proxy instead of OpenAI directly, and swapping providers later is a config change, not a code change. Here's a pattern that bit me early on. If your app answers common questions, "what's your refund policy," "how do I reset my password," you're paying full price to regenerate nearly identical answers over and over. Semantic caching fixes this by checking if a semantically similar prompt has already been answered, instead of requiring an exact string match. Semantic caching alone can cut costs 30 to 50 percent for repetitive workloads, and routing strategies combined with caching cut costs 40 to 70 percent overall. Stack a few optimization techniques together and you're looking at 50 to 80 percent reduction on production workloads. That's not a marginal tweak, that's the difference between a sustainable AI feature and one finance keeps asking you to justify. I cover more of this cost math in my LLM inference cost optimization https://mudassirkhan.me/blog/llm-inference-cost-optimization writeup if you want the full breakdown. python // lib/semantic-cache.ts import { createClient } from "redis"; import OpenAI from "openai"; const redis = createClient { url: process.env.REDIS URL } ; await redis.connect ; const embeddings = new OpenAI { apiKey: process.env.OPENAI API KEY } ; const SIMILARITY THRESHOLD = 0.95; async function getEmbedding text: string : Promise