Igniter is a modern, type-safe HTTP framework designed to streamline the development of scalable TypeScript applications. It combines the flexibility of traditional HTTP frameworks with the power of full-stack type safety, making it the ideal choice for teams building robust web applications.
- Type Safety Without Compromise: End-to-end type safety from your API routes to your client code, catching errors before they reach production
- Framework Agnostic: Seamlessly integrates with Next.js, Express, Fastify, or any Node.js framework
- Developer Experience First: Built with TypeScript best practices and modern development patterns in mind
- Production Ready: Being used in production by companies of all sizes
- Minimal Boilerplate: Get started quickly without sacrificing scalability
- Flexible Architecture: Adapts to your project's needs, from small APIs to large-scale applications
- π― Full TypeScript Support: End-to-end type safety from your API routes to your client code
- π Modern Architecture: Built with modern TypeScript features and best practices
- π Type-Safe Routing: Route parameters and query strings are fully typed
- π Middleware System: Powerful and flexible middleware support with full type inference
- π Context Sharing: Share context between middlewares and route handlers
- π Built-in Error Handling: Comprehensive error handling with type-safe error responses
- πͺ Cookie Management: Built-in cookie handling with signing support
- π¦ Framework Agnostic: Works with any Node.js framework (Express, Fastify, Next.js, etc.)
npm install @igniter-js/core
# or
yarn add @igniter-js/core
# or
pnpm add @igniter-js/core
# or
bun add @igniter-js/core
Building an API with Igniter is straightforward and intuitive. Here's how to get started:
Igniter promotes a feature-based architecture that scales with your application:
src/
βββ igniter.ts # Core initialization
βββ igniter.client.ts # Client implementation
βββ igniter.context.ts # Context management
βββ igniter.router.ts # Router configuration
βββ features/ # Application features
β βββ [feature]/
β βββ presentation/ # Feature presentation layer
β β βββ components/ # Feature-specific components
β β βββ hooks/ # Custom hooks
β β βββ contexts/ # Feature contexts
β β βββ utils/ # Utility functions
β βββ controllers/ # Feature controllers
β β βββ [feature].controller.ts
β βββ procedures/ # Feature procedures/middleware
β β βββ [feature].procedure.ts
β βββ [feature].interfaces.ts # Type definitions(interfaces, entities, inputs and outputs)
β βββ index.ts # Feature exports
- Feature-based Organization: Each feature is self-contained with its own controllers, procedures, and types
- Clear Separation of Concerns: Presentation, business logic, and data access are clearly separated
- Scalable Architecture: Easy to add new features without affecting existing ones
- Maintainable Codebase: Consistent structure makes it easy for teams to navigate and maintain
// src/igniter.ts
import { Igniter } from "@igniter-js/core";
import type { IgniterAppContext } from "./igniter.context";
/**
* @description Initialize the Igniter Router
* @see https://igniter.felipebarcelospro.github.io/docs/getting-started/installation
*/
export const igniter = Igniter.context<IgniterAppContext>().create()
// src/igniter.context
import { prisma } from "@/lib/db";
import { Invariant } from "@/utils";
/**
* @description Create the context of the application
* @see https://igniter.felipebarcelospro.github.io/docs/getting-started/installation
*/
export const createIgniterAppContext = () => {
return {
providers: {
database: prisma,
rules: Invariant.initialize('Igniter')
}
}
}
/**
* @description The context of the application
* @see https://igniter.felipebarcelospro.github.io/docs/getting-started/installation
*/
export type IgniterAppContext = Awaited<ReturnType<typeof createIgniterAppContext>>;
// src/features/user/controllers/user.controller.ts
import { igniter } from '@/igniter'
export const userController = igniter.controller({
path: '/users',
actions: {
// Query action (GET)
list: igniter.query({
path: '/',
use: [auth()],
query: z.object({
page: z.number().optional(),
limit: z.number().optional()
}),
handler: async (ctx) => {
return ctx.response.success({
users: [
{ id: 1, name: 'John Doe' }
]
})
}
}),
// Mutation action (POST)
create: igniter.mutation({
path: '/',
method: 'POST',
use: [auth()],
body: z.object({
name: z.string(),
email: z.string().email()
}),
handler: async (ctx) => {
const { name, email } = ctx.request.body
return ctx.response.created({
id: '1',
name,
email
})
}
})
}
})
// src/igniter.router.ts
import { igniter } from '@/igniter'
import { userController } from '@/features/user'
export const AppRouter = igniter.router({
baseURL: 'http://localhost:3000',
basePATH: '/api/v1',
controllers: {
users: userController
}
})
// Use with any HTTP framework
// Example with Express:
import { AppRouter } from '@/igniter.router'
app.use(async (req, res) => {
const response = await AppRouter.handler(req)
res.status(response.status).json(response)
})
// Example with Bun:
import { AppRouter } from '@/igniter.router'
Bun.serve({
fetch: AppRouter.handler
})
// Example with Next Route Handlers:
// src/app/api/v1/[[...all]]/route.ts
import { AppRouter } from '@/igniter.router'
import { nextRouteHandlerAdapter } from '@igniter-js/core/adapters'
export const { GET, POST, PUT, DELETE } = nextRouteHandlerAdapter(AppRouter)
The context system is the backbone of your application:
type AppContext = {
db: Database
user?: User
}
const igniter = Igniter.context<AppContext>().create()
- Keep context focused and specific to your application needs
- Use TypeScript interfaces to define context shape
- Consider splitting large contexts into domain-specific contexts
- Avoid storing request-specific data in global context
Procedures provide a powerful way to handle cross-cutting concerns:
import { igniter } from '@/igniter'
const auth = igniter.procedure({
handler: async (_, ctx) => {
const token = ctx.request.headers.get('authorization')
if (!token) {
return ctx.response.unauthorized()
}
const user = await verifyToken(token)
return { user }
}
})
// Use in actions
const protectedAction = igniter.query({
path: '/protected',
use: [auth()],
handler: (ctx) => {
// ctx.context.user is typed!
return ctx.response.success({ user: ctx.context.user })
}
})
- Authentication and Authorization
- Request Validation
- Logging and Monitoring
- Error Handling
- Performance Tracking
- Data Transformation
Controllers organize related functionality:
import { igniter } from '@/igniter'
const userController = igniter.controller({
path: 'users',
actions: {
list: igniter.query({
path: '/',
handler: (ctx) => ctx.response.success({ users: [] })
}),
get: igniter.query({
path: '/:id',
handler: (ctx) => {
// ctx.request.params.id is typed!
return ctx.response.success({ user: { id: ctx.request.params.id } })
}
})
}
})
- Group related actions together
- Keep controllers focused on a single resource or domain
- Use meaningful names that reflect the resource
- Implement proper error handling
- Follow RESTful conventions where appropriate
Igniter provides a robust response system:
handler: async (ctx) => {
// Success responses
ctx.response.success({ data: 'ok' })
ctx.response.created({ id: 1 })
ctx.response.noContent()
// Error responses
ctx.response.badRequest('Invalid input')
ctx.response.unauthorized()
ctx.response.forbidden('Access denied')
ctx.response.notFound('Resource not found')
// Custom responses
ctx.response.status(418).setHeader('X-Custom', 'value').json({ message: "I'm a teapot" })
}
Secure cookie handling made easy:
handler: async (ctx) => {
// Set cookies
await ctx.response.setCookie('session', 'value', {
httpOnly: true,
secure: true,
sameSite: 'strict'
})
// Set signed cookies
await ctx.response.setSignedCookie('token', 'sensitive-data', 'secret-key')
// Get cookies
const session = ctx.request.cookies.get('session')
const token = await ctx.request.cookies.getSigned('token', 'secret-key')
}
The Igniter React client provides a seamless integration with your frontend:
First, create your API client:
// src/igniter.client.ts
import { createIgniterClient, useIgniterQueryClient } from '@igniter-js/core/client';
import { AppRouter } from './igniter.router';
/**
* Client for Igniter
*
* This client is used to fetch data on the client-side
* It uses the createIgniterClient function to create a client instance
*
*/
export const api = createIgniterClient(AppRouter);
/**
* Query client for Igniter
*
* This client provides access to the Igniter query functions
* and handles data fetching with respect to the application router.
* It will enable the necessary hooks for query management.
*/
export const useQueryClient = useIgniterQueryClient<typeof AppRouter>;
Then, wrap your app with the Igniter provider:
// app/providers.tsx
import { IgniterProvider } from '@igniter-js/core/client'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<IgniterProvider>
{children}
</IgniterProvider>
)
}
Use the useQuery
hook for data fetching with automatic caching and revalidation:
import { api } from '@/igniter.client'
function UsersList() {
const listUsers = api.users.list.useQuery({
// Optional configuration
data: [], // Initial data while loading
params: {}, // Params for query
staleTime: 1000 * 60, // Data stays fresh for 1 minute
refetchInterval: 1000 * 30, // Refetch every 30 seconds
refetchOnWindowFocus: true, // Refetch when window regains focus
refetchOnMount: true, // Refetch when component mounts
refetchOnReconnect: true, // Refetch when reconnecting
onLoading: (isLoading) => console.log('Loading:', isLoading),
onRequest: (response) => console.log('Data received:', response)
})
if (loading) return <div>Loading...</div>
return (
<div>
<button onClick={() => refetch()}>Refresh</button>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
)
}
Use the useMutation
hook for data modifications:
function CreateUserForm() {
const createUser = api.users.create.useMutation({
// Optional configuration
defaultValues: { name: '', email: '' },
onLoading: (isLoading) => console.log('Loading:', isLoading),
onRequest: (response) => console.log('Created user:', response)
})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
await createUser.mutate({
body: {
name: 'John Doe',
email: '[email protected]'
}
})
// Handle success
} catch (error) {
// Handle error
}
}
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit" disabled={createUser.loading}>
{createUser.loading ? 'Creating...' : 'Create User'}
</button>
</form>
)
}
Invalidate queries manually or automatically after mutations:
function AdminPanel() {
const queryClient = useIgniterQueryClient()
// Invalidate specific queries
const invalidateUsers = () => {
queryClient.invalidate('users.list')
}
// Invalidate multiple queries
const invalidateAll = () => {
queryClient.invalidate([
'users.list',
'users.get'
])
}
return (
<button onClick={invalidateUsers}>
Refresh Users
</button>
)
}
The client provides full type inference for your API:
// All these types are automatically inferred
type User = InferOutput<typeof api.users.get>
type CreateUserInput = InferInput<typeof api.users.create>
type QueryKeys = InferCacheKeysFromRouter<typeof router>
// TypeScript will show errors for invalid inputs
api.users.create.useMutation({
onRequest: (data) => {
data.id // β
Typed as string
data.invalid // β TypeScript error
}
})
Use direct server calls with React Server Components:
// app/users/page.tsx
import { api } from '@/igniter.client'
export default async function UsersPage() {
const users = await api.users.list.call()
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
)
}
Use with Server Actions:
// app/users/actions.ts
'use server'
import { api } from '@/igniter.client'
export async function createUser(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
return api.users.create.call({
body: { name, email }
})
}
// app/users/create-form.tsx
export function CreateUserForm() {
return (
<form action={createUser}>
<input name="name" />
<input name="email" type="email" />
<button type="submit">Create User</button>
</form>
)
}
Combine Server and Client Components:
// app/users/hybrid-page.tsx
import { api } from '@/igniter.client'
// Server Component
async function UsersList() {
const users = await api.users.list.call()
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
)
}
// Client Component
'use client'
function UserCount() {
const { count } = api.users.count.useQuery()
return <div>Total Users: {count}</div>
}
// Main Page Component
export default function UsersPage() {
return (
<div>
<UserCount />
<Suspense fallback={<div>Loading...</div>}>
<UsersList />
</Suspense>
</div>
)
}
- Caching Strategy: Configure caching behavior per query
- Automatic Revalidation: Keep data fresh with smart revalidation
- Prefetching: Improve perceived performance
- Optimistic Updates: Provide instant feedback
- Parallel Queries: Handle multiple requests efficiently
function UserProfile() {
const { data, error, retry } = api.users.get.useQuery({
onError: (error) => {
console.error('Failed to fetch user:', error)
},
retry: 3, // Retry failed requests
retryDelay: 1000, // Wait 1 second between retries
})
if (error) {
return (
<div>
Error loading profile
<button onClick={retry}>Try Again</button>
</div>
)
}
return <div>{/* ... */}</div>
}
Use direct server calls with React Server Components:
// app/users/page.tsx
import { api } from '@/igniter.client'
export default async function UsersPage() {
const users = await api.users.list.query()
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
)
}
Igniter is designed with testability in mind:
import { router } from '@/igniter.router'
describe('User API', () => {
it('should create a user', async () => {
const result = await router.users.create.mutate({
body: {
name: 'Test User',
email: '[email protected]'
}
})
expect(result.status).toBe(201)
expect(result.data).toHaveProperty('id')
})
})
- Use procedures for authentication and authorization
- Implement rate limiting
- Validate all inputs
- Use secure cookie options
- Handle errors safely
- Implement CORS properly
import { igniter } from '@/igniter'
const monitor = igniter.procedure({
handler: async (_, ctx) => {
const start = performance.now()
// Wait for the next middleware/handler
const result = await ctx.next()
const duration = performance.now() - start
console.log(`${ctx.request.method} ${ctx.request.path} - ${duration}ms`)
return result
}
})
Recommended tsconfig.json
settings:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
We welcome contributions! Please see our contributing guidelines for details.
- π Documentation
- π Issue Tracker
- π€ Contributing Guidelines
MIT License - see the LICENSE file for details.