Router
Learn how to create and configure routers to assemble your API with full type safety, server-side calling, and HTTP handling.
Overview
The Router is the central entrypoint for all HTTP requests in your Igniter.js application. It maps incoming requests to your controllers and actions, providing both HTTP handling and type-safe server-side invocation.
import { igniter } from '@/igniter';
import { userController } from '@/features/users/user.controller';
import { postController } from '@/features/posts/post.controller';
export const AppRouter = igniter.router({
controllers: {
users: userController,
posts: postController
}
});What is a Router?
A Router connects your controllers (grouped actions) to HTTP endpoints and provides a type-safe caller for server-side invocation without HTTP overhead.
Creating a Router
Basic Router
The simplest way to create a router is with the igniter.router() method:
import { igniter } from '@/igniter';
import { userController } from './features/users/user.controller';
export const AppRouter = igniter.router({
controllers: {
users: userController
}
});This creates a router with:
- ✅ HTTP handler at
AppRouter.handler(request) - ✅ Type-safe caller at
AppRouter.caller.users.actionName() - ✅ Full type inference for all endpoints
Multiple Controllers
Group related features into separate controllers:
import { igniter } from '@/igniter';
import { userController } from './features/users/user.controller';
import { postController } from './features/posts/post.controller';
import { commentController } from './features/comments/comment.controller';
export const AppRouter = igniter.router({
controllers: {
users: userController,
posts: postController,
comments: commentController
}
});Each controller's path property is automatically prefixed to create full routes:
/users/*foruserController/posts/*forpostController/comments/*forcommentController
Router Properties
.handler(request)
The HTTP request handler that processes incoming requests and routes them to the appropriate action.
export const AppRouter = igniter.router({
controllers: { users: userController }
});
// Use in Next.js API route
export async function POST(request: Request) {
return AppRouter.handler(request);
}
// Use in Express
app.all('/api/*', async (req, res) => {
const request = convertToWebRequest(req);
const response = await AppRouter.handler(request);
return sendWebResponse(res, response);
});
// Use in Bun
Bun.serve({
port: 3000,
fetch: AppRouter.handler
});Request Processing Flow
graph LR
A[HTTP Request] --> B[Router.handler]
B --> C{Route Match?}
C -->|Yes| D[Run Procedures]
D --> E[Validate Input]
E --> F[Execute Action]
F --> G[Return Response]
C -->|No| H[404 Not Found].caller
The type-safe server-side caller allows you to invoke actions directly without HTTP overhead. Perfect for:
- ✅ Server-side rendering (SSR)
- ✅ Server components
- ✅ Integration tests
- ✅ Internal microservice calls
export const AppRouter = igniter.router({
controllers: {
users: userController,
posts: postController
}
});
// Server-side usage
const user = await AppRouter.caller.users.getById.query({ id: '123' });
const post = await AppRouter.caller.posts.create.mutate({
body: { title: 'Hello', content: 'World' }
});No HTTP Overhead
The .caller bypasses the HTTP layer entirely, directly invoking your action handlers with full type safety. It's significantly faster than HTTP calls!
Caller API
The caller API matches your action types:
For Query Actions:
// Query action (GET)
const result = await AppRouter.caller.users.list.query();
const user = await AppRouter.caller.users.getById.query({ id: '123' });For Mutation Actions:
// Mutation action (POST/PUT/DELETE/PATCH)
const created = await AppRouter.caller.users.create.mutate({
body: { name: 'John', email: 'john@example.com' }
});
const updated = await AppRouter.caller.users.update.mutate({
params: { id: '123' },
body: { name: 'Jane' }
});
const deleted = await AppRouter.caller.users.delete.mutate({
params: { id: '123' }
});Full Input Object
The caller accepts the same input structure as client calls:
const result = await AppRouter.caller.users.search.query({
query: { q: 'john', limit: 10 }, // Query parameters
headers: { 'x-api-key': 'secret' }, // Headers
cookies: { sessionId: 'abc123' }, // Cookies
credentials: 'include' // Credentials
});.controllers
Access to the registered controllers object:
const AppRouter = igniter.router({
controllers: {
users: userController,
posts: postController
}
});
console.log(AppRouter.controllers.users); // userController
console.log(AppRouter.controllers.posts); // postController.config
The router configuration including baseURL and basePATH:
const AppRouter = igniter.router({
controllers: { users: userController }
});
console.log(AppRouter.config.baseURL); // From Igniter config
console.log(AppRouter.config.basePATH); // From Igniter configFramework Integration
Next.js App Router
Create an API route that catches all requests:
// src/app/api/v1/[...path]/route.ts
import { AppRouter } from '@/igniter.router';
export async function GET(request: Request) {
return AppRouter.handler(request);
}
export async function POST(request: Request) {
return AppRouter.handler(request);
}
export async function PUT(request: Request) {
return AppRouter.handler(request);
}
export async function DELETE(request: Request) {
return AppRouter.handler(request);
}
export async function PATCH(request: Request) {
return AppRouter.handler(request);
}All HTTP methods route through .handler(), which automatically dispatches to the correct action based on path and method.
Next.js Pages Router
// pages/api/v1/[...path].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { AppRouter } from '@/igniter.router';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Convert Next.js request to Web Request
const request = new Request(
`http://localhost:3000${req.url}`,
{
method: req.method,
headers: req.headers as HeadersInit,
body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined
}
);
const response = await AppRouter.handler(request);
// Convert Web Response to Next.js response
const data = await response.json();
res.status(response.status).json(data);
}Express
import express from 'express';
import { AppRouter } from './igniter.router';
const app = express();
app.use(express.json());
// Catch-all route
app.all('/api/v1/*', async (req, res) => {
try {
const request = new Request(
`http://localhost:${port}${req.url}`,
{
method: req.method,
headers: req.headers as HeadersInit,
body: ['GET', 'HEAD'].includes(req.method) ? undefined : JSON.stringify(req.body)
}
);
const response = await AppRouter.handler(request);
const data = await response.json();
res.status(response.status).json(data);
} catch (error) {
res.status(500).json({ error: 'Internal Server Error' });
}
});
app.listen(3000);Bun
// server.ts
import { AppRouter } from './igniter.router';
Bun.serve({
port: 3000,
fetch: AppRouter.handler,
});
console.log('Server running at http://localhost:3000');Native Web APIs
Bun supports Web APIs natively, so you can use AppRouter.handler directly as the fetch handler!
TanStack Start
// app/routes/api.v1.$.ts
import { AppRouter } from '@/igniter.router';
export async function loader({ request }: { request: Request }) {
return AppRouter.handler(request);
}
export async function action({ request }: { request: Request }) {
return AppRouter.handler(request);
}Server-Side Usage
React Server Components (Next.js)
// app/users/page.tsx
import { AppRouter } from '@/igniter.router';
export default async function UsersPage() {
// Direct server-side call - no HTTP!
const { data } = await AppRouter.caller.users.list.query();
return (
<div>
<h1>Users</h1>
<ul>
{data.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}Server Actions (Next.js)
'use server';
import { AppRouter } from '@/igniter.router';
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const result = await AppRouter.caller.users.create.mutate({
body: { name, email }
});
return result;
}API Routes
// app/api/internal/sync-users/route.ts
import { AppRouter } from '@/igniter.router';
export async function POST() {
// Use caller for internal API routes
const users = await AppRouter.caller.users.list.query();
// Process users...
return Response.json({ success: true });
}Testing with Caller
The .caller is perfect for integration tests:
import { describe, it, expect } from 'vitest';
import { AppRouter } from '@/igniter.router';
describe('User API', () => {
it('should create a user', async () => {
const result = await AppRouter.caller.users.create.mutate({
body: {
name: 'Test User',
email: 'test@example.com'
}
});
expect(result.data.user).toMatchObject({
name: 'Test User',
email: 'test@example.com'
});
});
it('should list all users', async () => {
const result = await AppRouter.caller.users.list.query();
expect(result.data.users).toBeInstanceOf(Array);
});
it('should get user by ID', async () => {
// Create user first
const created = await AppRouter.caller.users.create.mutate({
body: { name: 'John', email: 'john@example.com' }
});
// Get by ID
const result = await AppRouter.caller.users.getById.query({
id: created.data.user.id
});
expect(result.data.user.name).toBe('John');
});
it('should handle not found', async () => {
await expect(
AppRouter.caller.users.getById.query({ id: 'non-existent' })
).rejects.toThrow();
});
});Fast Tests
Using .caller for tests is much faster than making HTTP requests. It directly invokes handlers with full type safety!
Type Inference
The router provides complete type inference for both HTTP and caller usage:
const AppRouter = igniter.router({
controllers: {
users: userController,
posts: postController
}
});
// ✅ Fully typed
type RouterType = typeof AppRouter;
// ✅ Extract caller type
type Caller = RouterType['caller'];
// ✅ Extract specific action
type UsersList = Caller['users']['list'];
// ✅ Use in functions
async function getUsers(): Promise<ReturnType<UsersList['query']>> {
return AppRouter.caller.users.list.query();
}Infer Context Type
type AppContext = typeof AppRouter.$Infer.$context;
// Result: The context type defined in Igniter.context<T>()Infer Plugins Type
type AppPlugins = typeof AppRouter.$Infer.$plugins;
// Result: The plugins type defined in Igniter.plugins<T>()Advanced Patterns
Multiple Routers
You can create multiple routers for different API versions or domains:
// v1 router
export const AppRouterV1 = igniter.router({
controllers: {
users: userControllerV1,
posts: postControllerV1
}
});
// v2 router
export const AppRouterV2 = igniter.router({
controllers: {
users: userControllerV2,
posts: postControllerV2,
analytics: analyticsController // New in v2
}
});// app/api/v1/[...path]/route.ts
export async function GET(request: Request) {
return AppRouterV1.handler(request);
}
// app/api/v2/[...path]/route.ts
export async function GET(request: Request) {
return AppRouterV2.handler(request);
}Router Composition
Create domain-specific routers and compose them:
// Public API router
export const PublicRouter = igniter.router({
controllers: {
auth: authController,
products: productsController
}
});
// Admin API router
export const AdminRouter = igniter.router({
controllers: {
users: adminUsersController,
analytics: analyticsController,
settings: settingsController
}
});Best Practices
1. Single Export
Export your router from a central file:
// src/igniter.router.ts
import { igniter } from '@/igniter';
import { userController } from '@/features/users/user.controller';
import { postController } from '@/features/posts/post.controller';
export const AppRouter = igniter.router({
controllers: {
users: userController,
posts: postController
}
});
export type AppRouterType = typeof AppRouter;2. Use Caller for Server-Side
Always prefer .caller over HTTP requests when calling from the server:
// ❌ Bad - HTTP overhead on same server
const response = await fetch('http://localhost:3000/api/v1/users');
const data = await response.json();
// ✅ Good - Direct invocation, faster and type-safe
const { data } = await AppRouter.caller.users.list.query();3. Group Related Controllers
Organize controllers by feature or domain:
export const AppRouter = igniter.router({
controllers: {
// User management
users: userController,
profiles: profileController,
// Content
posts: postController,
comments: commentController,
// Admin
analytics: analyticsController,
settings: settingsController
}
});4. Type-Safe Error Handling
try {
const result = await AppRouter.caller.users.getById.query({ id: '123' });
console.log(result.data.user);
} catch (error) {
// Errors are automatically thrown from .caller
console.error('User not found:', error);
}Debugging
Enable Request Logging
The router automatically logs requests when in development mode. You can also access logs programmatically:
const response = await AppRouter.handler(request);
// Logs include:
// - Incoming request (URL, method, headers)
// - Parsed request (path, basePath, queryParams)
// - Routing decision (controller, action)
// - Response (status, headers, body)Inspect Router Structure
console.log(AppRouter.controllers); // All controllers
console.log(AppRouter.config); // Router config
console.log(AppRouter.caller); // Caller proxyNext Steps
Now that you understand routers, dive deeper into the building blocks: