How to Structure Complex API Logic in 9router: A Modular Architecture Guide
Structure complex API logic in 9router by isolating Next.js route handlers, modular MITM proxy logic in src/mitm/handlers/, and provider-specific translators using the open-sse/translator registry.
9router is an open-source routing layer that normalizes requests between OpenAI-compatible clients and diverse AI providers. When you structure complex API logic in 9router following its established patterns, you create maintainable, streaming-ready endpoints without entangling business logic in your main application code.
Core Architectural Components
The recommended architecture separates concerns into three distinct layers that communicate through strict contracts.
Thin API Entry Points in Next.js Routes
Route files under src/app/api/v1/*/route.js should remain minimal delegators. They ensure initialization and forward requests to specialized handlers, keeping HTTP transport concerns separate from business logic.
Example from src/app/api/v1/chat/completions/route.js:
import { ensureInitialized } from './route.js';
import { handleCompletions } from '@/sse/handlers/completions.js';
export async function POST(request) {
await ensureInitialized();
return await handleCompletions(request);
}
See the full implementation at src/app/api/v1/chat/completions/route.js.
MITM Handlers for Proxy Logic
Complex forwarding logic belongs in src/mitm/handlers/. These modules consume the base utilities fetchRouter and pipeSSE from src/mitm/handlers/base.js to manage request forwarding and Server-Sent Events (SSE) streaming.
In src/mitm/handlers/kiro.js, the pattern demonstrates intercepting requests, applying transformations, and streaming responses:
const { fetchRouter, pipeSSE } = require('./base');
const { translateRequest } = require('../translator/index');
async function handleKiro(req, res) {
const body = await req.text();
const translated = translateRequest(body, 'kiro');
const routerRes = await fetchRouter(translated, '/v1/chat/completions', req.headers);
await pipeSSE(routerRes, res);
}
Reference the complete handler at src/mitm/handlers/kiro.js.
The Translator Registry for Format Normalization
The open-sse/translator/index.js file maintains a registry that maps OpenAI-compatible payloads to provider-native formats. Request translators live in open-sse/translator/request/ and response translators in open-sse/translator/response/.
To register a new provider, implement a pure transformation function and register it via FORMATS.registerRequest():
// open-sse/translator/request/openai-to-claude.js
import { FORMATS } from '../formats.js';
export function openAiToClaude(openaiBody) {
return {
prompt: openaiBody.messages[0].content,
max_tokens: openaiBody.max_tokens
};
}
FORMATS.registerRequest('claude', openAiToClaude);
View the registration pattern at open-sse/translator/request/openai-to-claude.js.
Recommended Implementation Workflow
Follow these steps to structure complex API logic in 9router when adding new providers or endpoints.
-
Create a minimal route file under
src/app/api/v1/<feature>/route.jsthat importsensureInitializedfrom the completions route and delegates to your handler. -
Build a dedicated MITM handler in
src/mitm/handlers/<name>.js. ImportfetchRouterandpipeSSEfrom./baseto handle HTTP forwarding and SSE streaming. CalltranslateRequest()before forwarding andtranslateResponse()after receiving the upstream response. -
Implement translator pairs if the provider uses non-OpenAI schemas. Place request normalizers in
open-sse/translator/request/and response denormalizers inopen-sse/translator/response/. Each file should export a pure function and register it with theFORMATSobject exported from the registry. -
Access shared state through stores rather than hardcoding configuration. Import
providerStorefromsrc/store/providerStore.jsto retrieve provider metadata, API keys, and user-specific settings within your handlers.
Practical Code Example
Here is a complete implementation of a custom provider endpoint following 9router’s architectural conventions.
Route layer (src/app/api/v1/custom/route.js):
import { ensureInitialized } from '@/app/api/v1/chat/completions/route.js';
import { handleCustomProvider } from '@/mitm/handlers/custom.js';
export async function POST(request) {
await ensureInitialized();
return await handleCustomProvider(request);
}
Handler layer (src/mitm/handlers/custom.js):
const { fetchRouter, pipeSSE } = require('./base');
const { translateRequest, translateResponse } = require('../translator/index');
async function handleCustomProvider(req, res) {
const body = await req.text();
const upstreamBody = translateRequest(body, 'customProvider');
const upstreamRes = await fetchRouter(upstreamBody, '/v1/chat/completions', req.headers);
await pipeSSE(upstreamRes, res);
}
module.exports = { handleCustomProvider };
Translator layer (open-sse/translator/request/customProvider.js):
import { FORMATS } from '../formats.js';
function customProviderRequest(openaiPayload) {
return {
query: openaiPayload.messages.map(m => m.content).join('\n'),
temperature: openaiPayload.temperature
};
}
FORMATS.registerRequest('customProvider', customProviderRequest);
Summary
- Structure complex API logic in 9router by keeping Next.js routes thin and delegating to MITM handlers.
- Place request/response format conversions in the
open-sse/translator/registry using pure functions registered viaFORMATS.registerRequest()andFORMATS.registerResponse(). - Reuse
fetchRouterandpipeSSEfromsrc/mitm/handlers/base.jsto ensure consistent proxy behavior and SSE streaming support. - Initialize translators once using
ensureInitialized()to prevent duplicate registration side effects. - Store provider configuration in
src/store/providerStore.jsrather than hardcoding credentials or endpoints in handlers.
Frequently Asked Questions
How do I add support for a new AI provider in 9router?
Create a new request translator in open-sse/translator/request/ that maps OpenAI fields to the provider's schema, register it with FORMATS.registerRequest(), and implement a MITM handler in src/mitm/handlers/ that invokes translateRequest() before calling fetchRouter(). This keeps provider logic isolated and reusable across endpoints.
Where should I place business logic that transforms API responses?
Use the response translator pattern by adding files to open-sse/translator/response/. These modules should export pure functions that convert provider-specific payloads back to OpenAI-compatible formats, registering them via FORMATS.registerResponse() in the translator index. This separation ensures response normalization remains testable and provider-agnostic.
What is the purpose of the ensureInitialized() function?
ensureInitialized(), defined in src/app/api/v1/chat/completions/route.js, guarantees that initTranslators() runs exactly once per application instance. This prevents duplicate translator registration and ensures the FORMATS registry is fully populated before any request handling occurs, avoiding runtime errors during translation.
Can I use standard Node.js HTTP clients instead of fetchRouter?
While possible, using fetchRouter from src/mitm/handlers/base.js is recommended because it handles credential injection from providerStore, request signing, and response streaming consistently across all providers. It ensures your complex API logic integrates seamlessly with 9router's MITM architecture and supports transparent SSE piping through pipeSSE().
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →