# Igniter.js > Igniter is a modern, type-safe HTTP framework designed to streamline the development of scalable TypeScript applications. This documentation provides comprehensive information about Igniter.js, a modern, type-safe HTTP framework for TypeScript applications. The content is organized to help LLMs understand the framework's architecture, features, and usage patterns. ## Project Information - **Framework**: Igniter.js - **Language**: TypeScript - **Type**: HTTP Framework - **Focus**: Type-safety, Developer Experience, Performance ## Getting Started - [Installation](/docs/getting-started/installation): Documentation page - [Quick Start Guide](/docs/getting-started/quick-start-guide): Documentation page - [Recommended Project Structure](/docs/getting-started/project-structure): Documentation page ## Core Concepts - [Context: The Heart of Your Application's State](/docs/core-concepts/context): Documentation page - [Controllers & Actions: Building Your API Logic](/docs/core-concepts/controllers-and-actions): Documentation page - [Procedures in Igniter.js](/docs/core-concepts/procedures): Documentation page - [Routing: Assembling Your API](/docs/core-concepts/routing): Documentation page - [The Igniter Builder: Your Application's Foundation](/docs/core-concepts/igniter-builder): Documentation page - [Validation: Ensuring Data Integrity and Business Rules](/docs/core-concepts/validation): Documentation page ## Client-Side Integration - [Client-Side: Fetching Data with `useQuery`](/docs/client-side/use-query): Documentation page - [Client-Side: Modifying Data with `useMutation`](/docs/client-side/use-mutation): Documentation page - [Client-Side: Subscribing with `useRealtime`](/docs/client-side/use-realtime): Documentation page - [Client-Side: The ``](/docs/client-side/igniter-provider): Documentation page - [Client-Side: The Type-Safe API Client](/docs/client-side/api-client): Documentation page ## Starter Guides - [Full-Stack Guide: Building a High-Performance SPA with Bun, React, and Igniter.js](/docs/starter-guides/bun-react-starter-guide): Documentation page - [Full-Stack Guide: Building with the Igniter.js Next.js Starter](/docs/starter-guides/nextjs-starter-guide): Documentation page - [Full-Stack Guide: Building with the Igniter.js TanStack Start Starter](/docs/starter-guides/tanstack-start-starter-guide): Documentation page - [Guide: Building High-Performance, Type-Safe REST APIs with Igniter.js](/docs/starter-guides/rest-api-starter-guide): Documentation page ## CLI and Tooling - [`igniter generate`: Scaffolding & Schema Generation](/docs/cli-and-tooling/igniter-generate): Documentation page - [CLI: Scaffolding with `igniter init`](/docs/cli-and-tooling/igniter-init): Documentation page - [CLI: The Interactive Dev Server `igniter dev`](/docs/cli-and-tooling/igniter-dev): Documentation page ## Advanced Features - [Igniter Studio (API Playground)](/docs/advanced-features/igniter-studio): Documentation page - [Igniter.js Queues: Reliable Background Processing](/docs/advanced-features/queues): Documentation page - [Igniter.js Realtime: Live Data, Effortlessly](/docs/advanced-features/realtime): Documentation page - [Igniter.js Store: High-Performance Caching and Messaging](/docs/advanced-features/store): Documentation page - [OpenAPI Documentation](/docs/advanced-features/openapi-documentation): Documentation page ## code-agents - [Guiding LLMs with llms.txt](/docs/code-agents/llms-txt): Documentation page - [Initialize a Project with Lia](/docs/code-agents/initialize-project): Documentation page - [Introduction to Code Agents](/docs/code-agents/introduction): Documentation page - [Using Claude with Igniter.js](/docs/code-agents/claude-code): Documentation page - [Using Cursor with Igniter.js](/docs/code-agents/cursor): Documentation page - [Using Google's Gemini CLI with Igniter.js for AI-Powered Development](/docs/code-agents/gemini-cli): Documentation page - [Using VS Code Copilot with Igniter.js](/docs/code-agents/vscode-copilot): Documentation page - [Using Windsurf with Igniter.js for AI-Powered Development](/docs/code-agents/windsurf): Documentation page - [Using Zed Editor with Igniter.js for AI-Powered Development](/docs/code-agents/zed-editor): Documentation page ## Full Documentation Content Below is the complete content of all documentation pages for comprehensive LLM understanding: ### Installation **Category**: getting-started **URL**: /docs/getting-started/installation # Installation Welcome to Igniter.js! This guide provides everything you need to get started, whether you're building a new application from the ground up or integrating Igniter.js into an existing project. Our goal is to get you running in minutes. ## 1. Start with a Template The fastest and most effective way to start a new project is with one of our official templates. These are production-ready boilerplates, meticulously configured with best practices, end-to-end type safety, and all the essential tooling. They embody our development philosophy, saving you valuable setup time. ### Official Project Starters ## 2. Use the `igniter init` CLI If our templates don't perfectly match your requirements, the `igniter init` command is your best alternative. This interactive CLI scaffolds a clean, structured project, giving you the freedom to choose your own integrations while still benefiting from our proven project setup. ```bash npx @igniter-js/cli init my-new-app ``` ```bash pnpm dlx @igniter-js/cli init my-new-app ``` ```bash bunx @igniter-js/cli init my-new-app ``` For a detailed walkthrough of the CLI, please see our **[Quick Start Guide](/docs/getting-started/quick-start-guide)**. ## 3. Manual Installation for Existing Projects For those who wish to integrate Igniter.js into an existing codebase, a manual setup is the way to go. This approach allows you to adopt our framework incrementally, without needing to refactor your entire project. Simply install the core package using your preferred package manager: ```bash npm install @igniter-js/core ``` ```bash yarn add @igniter-js/core ``` ```bash pnpm add @igniter-js/core ``` ```bash bun add @igniter-js/core ``` This single package gives you access to the core builder, router, and all the foundational tools needed to bring type-safe APIs to your application. ## 4. Optional: Add Adapters and Dependencies Igniter.js features a powerful, adapter-based architecture. This means advanced functionalities like caching, background jobs, and observability are handled by separate, dedicated packages. This keeps the framework's core lightweight and ensures you only install what you truly need. ### Store Adapter (Caching & Pub/Sub) For a high-performance Redis-based store, install the adapter and its peer dependencies. For a seamless TypeScript experience, we also recommend installing the type definitions. ```bash npm install @igniter-js/adapter-redis ioredis npm install @types/ioredis --save-dev ``` ```bash yarn add @igniter-js/adapter-redis ioredis yarn add @types/ioredis --dev ``` ### Queues Adapter (Background Jobs) To enable robust background job processing powered by BullMQ, install the official adapter and its dependency. ```bash npm install @igniter-js/adapter-bullmq bullmq ``` ```bash yarn add @igniter-js/adapter-bullmq bullmq ``` ### Telemetry Adapter (OpenTelemetry) For comprehensive observability, install our OpenTelemetry adapter and its required peer dependencies. ```bash npm install @igniter-js/adapter-opentelemetry @opentelemetry/api @opentelemetry/sdk-node ``` ```bash yarn add @igniter-js/adapter-opentelemetry @opentelemetry/api @opentelemetry/sdk-node ``` ### MCP Server Adapter (AI Code Agents) To transform your API into a set of executable tools for AI code agents, install our Model-Context Protocol (MCP) Server adapter. ```bash npm install @igniter-js/adapter-mcp-server @vercel/mcp-adapter @modelcontextprotocol/sdk ``` ```bash yarn add @igniter-js/adapter-mcp-server @vercel/mcp-adapter @modelcontextprotocol/sdk ``` ### Validation with Zod While optional, we highly recommend using `zod` for validating request bodies, queries, and parameters. It is deeply integrated into the Igniter.js type system. ```bash npm install zod ``` ```bash yarn add zod ``` --- ## Next Steps With Igniter.js installed, you're all set to start building. Here are some great next steps: - **[Quick Start Guide](/docs/getting-started/quick-start-guide)**: Build your first API endpoint in under 5 minutes. - **[Project Structure](/docs/getting-started/project-structure)**: Learn our recommended approach to organizing your project. - **[Core Concepts](/docs/core-concepts/the-igniter-builder)**: Dive deep into the fundamental building blocks of the framework. --- ### Quick Start Guide **Category**: getting-started **URL**: /docs/getting-started/quick-start-guide # Quick Start Guide Welcome to the Igniter.js Quick Start Guide! This tutorial provides a detailed, step-by-step walkthrough to build your first fully type-safe API endpoint. We'll go from an empty directory to a running server, explaining each concept along the way. ## Prerequisites Before we begin, please ensure you have the following installed on your system: * **Node.js**: Version 18.x or higher. * **A Package Manager**: This guide provides commands for `npm`, `pnpm`, and `bun`. ## Step 1: Create Your Igniter.js Project We'll start by using the official `igniter init` command, which scaffolds a new, production-ready project with a logical folder structure and all necessary configurations. Open your terminal and run the command below using your preferred package manager: ```bash npx @igniter-js/cli init my-first-api ``` ```bash pnpm dlx @igniter-js/cli init my-first-api ``` ```bash bunx @igniter-js/cli init my-first-api ``` This command creates a new directory called `my-first-api`, installs dependencies, and sets up your project. The `igniter init` command created a starter project that includes the Igniter.js core, essential configuration files (`igniter.ts`, `igniter.router.ts`), and a logical, feature-based directory structure under `src/`. This setup is designed for scalability and maintainability. Once the process is complete, navigate into your new project directory: ```bash cd my-first-api ``` ## Step 2: Create Your First Controller In Igniter.js, a `Controller` is a file that groups related API endpoints. These endpoints are called `Actions` (either a `Query` for GET requests or a `Mutation` for POST, PUT, DELETE, etc.). Let's create a "hello world" controller. First, create the necessary folders: ```bash mkdir -p src/features/greeting/controllers ``` Now, create a new file at `src/features/greeting/controllers/greeting.controller.ts` and add the following code: ```typescript // src/features/greeting/controllers/greeting.controller.ts import { igniter } from '@/igniter'; import { z } from 'zod'; export const greetingController = igniter.controller({ name: 'GreetingController', path: '/greetings', actions: { hello: igniter.query({ query: z.object({ name: z.string().optional().default('World'), }), handler: ({ request, response }) => { const { name } = request.query; return response.success({ message: `Hello, ${name}!` }); }, }), }, }); ``` ### Understanding the Code: A Properties Breakdown To understand what we just wrote, you can expand the sections below to see a detailed breakdown of the properties for both the `controller` and the `actions` within it. A descriptive name for the controller, recommended for clarity and debugging. The base URL segment for all actions within this controller. For example, `/greetings`. A high-level summary of the controller's purpose, useful for documentation. An object containing all the API endpoints (`Actions`) for this controller.

Igniter.js has two types of actions: `igniter.query()` for data fetching (GET) and `igniter.mutation()` for data modification (POST, PUT, DELETE). They share some properties but have key differences.

**Query Action (`igniter.query`)** A descriptive name for the action, useful for DevTools and documentation. A summary of what the action does. The URL segment for this action, appended to the controller's path. Final URL: `/greetings/hello`. A Zod schema to validate URL query parameters. For more information on Zod, see the official Zod documentation. An array of middleware to run before the handler. The function containing your business logic, receiving `request` and `response` in its context. **Mutation Action (`igniter.mutation`)** A descriptive name for the action. A summary of what the action does. The URL segment for this action. The HTTP method to use, e.g., `'POST'`, `'PUT'`, `'DELETE'`. A Zod schema to validate the incoming request body (JSON). A Zod schema to validate URL query parameters. Yes, mutations can have them too! An array of middleware to run before the handler. The function containing your logic.
## Step 3: Register the Controller with the Router Your controller is ready, but the application doesn't know about it yet. We need to register it in the main router. Open `src/igniter.router.ts` and modify it to include your new controller: ```typescript // src/igniter.router.ts import { igniter } from '@/igniter'; // 1. Import your new controller import { greetingController } from '@/features/greeting/controllers/greeting.controller'; export const AppRouter = igniter.router({ controllers: { // 2. Register the controller under a key. // This key is used for client-side type inference. greetings: greetingController, }, }); // This export is crucial for client-side type safety! export type AppRouterType = typeof AppRouter; ``` The `AppRouter` is the heart of your application's API. It aggregates all your controllers and defines the overall shape of your API. By exporting its *type*, you enable the Igniter.js client to have fully type-safe access to your backend. ## Step 4: Run the Development Server Igniter.js includes an interactive development server that provides real-time feedback and a dashboard for your API. Start the server by running the following command in your terminal: ```bash npm run dev ``` This command executes `igniter dev --interactive`. You should see a dashboard in your terminal, confirming that the server is running successfully on `http://localhost:3000`. ## Step 5: Test Your API Endpoint Your API is now live and ready to be tested! You can use a tool like `cURL` or simply open the URL in your web browser. Open a new terminal window and run this command: ```bash # Test with the default name ('World') curl http://localhost:3000/api/v1/greetings/hello ``` You should see the following JSON response: ```json {"message":"Hello, World!"} ``` Now, let's provide a custom name via the query string: ```bash # Test with a custom name curl "http://localhost:3000/api/v1/greetings/hello?name=Igniter" ``` And the response will be: ```json {"message":"Hello, Igniter!"} ``` ## Congratulations! You have successfully built and tested your first fully type-safe API endpoint with Igniter.js! In this guide, you have learned how to: - **Scaffold a project** using `igniter init`. - **Create a `Controller`** to group related API actions. - **Define a `Query Action`** with input validation using Zod. - **Register the controller** with the main application `Router`. - **Run the interactive development server** and test your endpoint. ## Ready for a Real Project? You've learned the basics, now it's time to build something more substantial. Our official starter templates are the perfect way to kickstart your next project with a production-ready foundation. ### Next Steps * **[Core Concepts](/docs/core-concepts/the-igniter-builder)**: Take a deep dive into the fundamental building blocks of the framework. * **[Routing in Depth](/docs/core-concepts/routing)**: Learn more about file-based routing, parameters, and advanced routing techniques. * **[Project Structure](/docs/getting-started/project-structure)**: Understand our best practices for organizing large and scalable applications. --- ### Recommended Project Structure **Category**: getting-started **URL**: /docs/getting-started/project-structure # Recommended Project Structure A well-organized project structure is crucial for building scalable, maintainable, and collaborative applications. Igniter.js promotes a **Feature-Sliced Architecture** that is designed to grow with your project, keeping your codebase clean and easy to navigate. The `igniter init` command automatically scaffolds a project with this structure. --- ## The Feature-Based Philosophy Instead of organizing your code by file type (e.g., a single folder for all controllers, another for all services), we organize it by **feature**. A feature is a self-contained vertical slice of your application's functionality, such as `users`, `products`, or `auth`. This approach has several key benefits: * **High Cohesion**: All code related to a single feature (its routes, logic, types, etc.) lives together. * **Low Coupling**: Features are isolated and have minimal dependencies on each other, making them easier to develop, test, and remove if needed. * **Scalability**: As your application grows, you simply add new feature folders without cluttering existing ones. * **Improved Developer Experience**: It's intuitive to find the code you're looking for because it's grouped by its business purpose. --- ## Top-Level Directory Structure Here is the recommended top-level structure for an Igniter.js project: ``` src/ ├── app/ │ └── api/ │ └── v1/ │ └── [[...all]]/ │ └── route.ts # Framework-specific route handler (e.g., Next.js) ├── features/ # ★ Your application's features live here │ └── [feature]/ │ ├── controllers/ │ ├── procedures/ │ ├── [feature].interfaces.ts │ └── index.ts ├── services/ # Third-party service initializations (Prisma, Redis) │ ├── database.ts │ └── redis.ts ├── igniter.ts # Core Igniter.js instance initialization ├── igniter.client.ts # Type-safe client for frontend use ├── igniter.context.ts # Global application context definition └── igniter.router.ts # The main application router where all controllers are assembled ``` ### Explanation of Directories * **`src/app`**: Contains framework-specific integration files. In a Next.js project, this is where the API route handler lives. Igniter.js itself is framework-agnostic, but it needs a single entry point. * **`src/features`**: The heart of your application. Each subdirectory within `features` represents a distinct business capability. * **`src/services`**: A dedicated place to initialize and export instances of external services, such as a database client (`Prisma`), a Redis client, or a logger. * **`src/igniter.ts`**: Where you create and configure the core `Igniter` instance, enabling plugins and global middleware. * **`src/igniter.context.ts`**: Defines the shape of the global `Context` object that is available in all your actions and procedures. * **`src/igniter.router.ts`**: Where you import all your feature controllers and assemble them into the final `AppRouter`. * **`src/igniter.client.ts`**: Defines the type-safe client used by your frontend application to interact with the API. --- ## Inside a Feature Directory Let's look at the structure of a single feature, for example, `src/features/user`: ``` features/ └── user/ ├── controllers/ │ └── user.controller.ts # Defines API endpoints (/users, /users/:id) ├── procedures/ │ └── auth.procedure.ts # Reusable middleware (e.g., for checking authentication) ├── user.interfaces.ts # Zod schemas and TypeScript types for the User feature └── index.ts # Exports the feature's public modules (e.g., userController) ``` * **`controllers/`**: Contains one or more controller files that define the API routes for the feature using `igniter.controller`. * **`procedures/`**: Contains reusable middleware created with `igniter.procedure`. For example, an `auth` procedure here could be used to protect user-related routes. * **`[feature].interfaces.ts`**: A central file for all TypeScript `interface` or `type` definitions and `zod` schemas related to this feature. This keeps your data shapes explicit and organized. * **`index.ts`**: The public entry point for the feature. It typically exports the controllers so they can be easily imported into the main router. By following this structure, you create a codebase that is organized, scalable, and a pleasure to work on. --- ## Next Steps Now that you understand the structure, let's dive into the core concepts of Igniter.js: * **[The Igniter Builder](/docs/core-concepts/the-igniter-builder)** - Learn about the foundation of every Igniter.js application * **[Context](/docs/core-concepts/context)** - Understand dependency injection and shared state * **[Controllers & Actions](/docs/core-concepts/controllers-and-actions)** - Build your API endpoints --- ### Context: The Heart of Your Application's State **Category**: core-concepts **URL**: /docs/core-concepts/context # Context: The Heart of Your Application's State In Igniter.js, the **Context** is an object that is available in every API action and procedure. Its primary purpose is to act as a powerful, type-safe **Dependency Injection (DI)** mechanism. It holds all the services, data, and helpers your application needs to process a request, such as a database connection, the current user's session, or a logging instance. Unlike the context in some other frameworks which is often a simple, static object, the context in Igniter.js is **dynamic and composable**. It starts with a base shape and is progressively enriched by middleware (Procedures), creating a tailored, fully-typed context for each specific action. ## 1. The Base Application Context (`AppContext`) Everything starts with the base context. This is the global state that should be available to your entire application. You define its shape and creation logic in `src/igniter.context.ts`. **Why it's important:** This file establishes a single source of truth for your application's core dependencies. **Example: Defining a Context with a Database Connection** Let's create a base context that provides a Prisma database client. ```typescript // src/services/database.ts import { PrismaClient } from '@prisma/client'; // Initialize the client once and export it export const database = new PrismaClient(); // src/igniter.context.ts import { database } from '@/services/database'; /** * A function that returns the base context object. * This function will be called for every incoming request. */ export const createIgniterAppContext = () => { return { database, // Provide the database client to the context }; }; /** * The TypeScript type of our base context. * We infer it directly from the creation function to ensure they are always in sync. */ export type IgniterAppContext = ReturnType; ``` This `IgniterAppContext` type is then passed to the Igniter Builder in `src/igniter.ts` to set the foundation for your application's type system: ```typescript // src/igniter.ts import { Igniter } from '@igniter-js/core'; import type { IgniterAppContext } from './igniter.context'; export const igniter = Igniter .context() // Setting the base context type // ... other configurations .create(); ``` ## 2. Accessing Context in an Action Once defined, the base context is available in the `handler` of every action via the `ctx` (context) argument. ```typescript // src/features/user/controllers/user.controller.ts import { igniter } from '@/igniter'; export const userController = igniter.controller({ path: '/users', actions: { list: igniter.query({ path: '/', handler: async ({ context, response }) => { // The `context` object is fully typed! // TypeScript knows `context.database` exists and what methods it has. const users = await context.database.user.findMany(); return response.success({ users }); }, }), }, }); ``` Because we defined `database` in our `IgniterAppContext`, TypeScript provides full autocompletion and type-checking for `context.database`. ## 3. The Magic: A Dynamic, Extendable Context Here is where Igniter.js truly shines. The context passed to your action handler is not just the base context; it's a **merged object** composed of the base context **plus** any data returned by the procedures (middleware) used in that action. ### Extending Context with Procedures A **Procedure** can return an object from its handler. This return value is then deeply merged into the context of the next procedure in the chain, and ultimately, into the context of the final action handler. **Use Case: An Authentication Procedure** Let's create a procedure that verifies a user's token and adds the `user` object to the context. ```typescript // src/features/auth/procedures/auth.procedure.ts import { igniter } from '@/igniter'; import { verifyToken } from '@/services/auth'; // Your token verification logic export const auth = igniter.procedure({ handler: async ({ request, response }) => { const token = request.headers.get('Authorization')?.split(' ')[1]; if (!token) { return response.unauthorized({ message: 'No token provided' }); } const userPayload = await verifyToken(token); if (!userPayload) { return response.unauthorized({ message: 'Invalid token' }); } // This is the magic! // We return an object that will be merged into the context. return { // The key 'user' will be added to the final context object. user: { id: userPayload.id, email: userPayload.email, }, }; }, }); ``` ### Using the Extended Context Now, let's use this `auth` procedure in a protected route. ```typescript // src/features/user/controllers/user.controller.ts import { igniter } from '@/igniter'; import { auth } from '@/features/auth/procedures/auth.procedure'; // 1. Import the procedure export const userController = igniter.controller({ path: '/users', actions: { getProfile: igniter.query({ path: '/me', // 2. Apply the procedure to this action use: [auth], handler: async ({ context, response }) => { // 3. Access the extended context! // TypeScript knows `context.user` exists because the `auth` procedure provides it. // It also still knows about `context.database` from the base context. const currentUser = context.user; const userDetails = await context.database.user.findUnique({ where: { id: currentUser.id }, }); return response.success({ profile: userDetails }); }, }), }, }); ``` Notice that we didn't have to manually tell TypeScript that `context.user` exists. Igniter.js infers this automatically from the `use: [auth]` array. The final context for the `getProfile` handler is a merged type of `IgniterAppContext & { user: { id: string; email: string; } }`. ## The Final Context Object For any given request, the context is built layer by layer, ensuring perfect type safety and data isolation at each step. 1. **Base Context:** The request starts with the global `IgniterAppContext` (e.g., `{ database }`). 2. **Procedure 1:** A procedure runs, returns `{ a: 1 }`. The context becomes `{ database, a: 1 }`. 3. **Procedure 2:** Another procedure runs, returns `{ b: 2 }`. The context becomes `{ database, a: 1, b: 2 }`. 4. **Action Handler:** The final handler receives the fully merged and typed context: `{ database, a: 1, b: 2 }`. This powerful, composable pattern allows you to build clean, decoupled, and highly testable business logic. --- **Next Steps** Now that you understand how to manage state and dependencies with Context, let's see how to structure your API endpoints: - **[Controllers & Actions](/docs/core-concepts/controllers-and-actions)** - Learn about API endpoint structure - **[Procedures](/docs/core-concepts/procedures)** - Deep dive into middleware - **[Validation](/docs/core-concepts/validation)** - Type-safe input validation --- ### Controllers & Actions: Building Your API Logic **Category**: core-concepts **URL**: /docs/core-concepts/controllers-and-actions # Controllers & Actions: Building Your API Logic At the core of every Igniter.js application are **Controllers** and **Actions**. This is where you define your API's endpoints, implement your business logic, and handle interactions with your data and services. **Controllers** organize related API endpoints, while **Actions** define individual endpoints with their business logic. Together, they provide a clean, maintainable, and scalable way to build your API.
Organizational units that group related Actions together with a shared base path and configuration. Individual API endpoints that handle specific requests like fetching data or creating resources.
## 1. Controllers: Organizing Your Endpoints A Controller is created using the `igniter.controller()` factory function. Its primary role is to define a base `path` that acts as a prefix for all the Actions it contains. ### Controller Anatomy Every controller follows the `IgniterControllerConfig` interface: | Property | Type | Required | Description | |:---------|:-----|:---------|:------------| | `name` | `string` | ❌ | Optional controller name for documentation and API introspection | | `path` | `string` | ✅ | Base URL path prefix for all actions in this controller | | `description` | `string` | ❌ | Optional description for documentation generation and OpenAPI spec | | `actions` | `Record` | ✅ | Collection of actions where keys are action names and values are action definitions | Controllers are fully type-safe. The `actions` object is validated at compile-time to ensure all actions conform to the proper structure. ### Complete Controller Example ```typescript // src/features/user/controllers/user.controller.ts import { igniter } from '@/igniter'; import { z } from 'zod'; export const userController = igniter.controller({ /** * Optional name for the controller * Useful for documentation generation and MCP Server tool conversion */ name: 'UserController', /** * The base path for all actions in this controller. * All action paths will be prefixed with `/users`. */ path: '/users', /** * Optional description for documentation generation * Essential for OpenAPI spec generation and MCP Server AI agent integration */ description: 'Handles all user-related operations including CRUD and authentication', /** * A collection of all API endpoints related to users. * Each key becomes an action name, each value defines the endpoint. */ actions: { list: igniter.query({ /* ... */ }), getById: igniter.query({ /* ... */ }), create: igniter.mutation({ /* ... */ }), update: igniter.mutation({ /* ... */ }), delete: igniter.mutation({ /* ... */ }), }, }); ``` ### Controller Properties | Property | Type | Required | Description | |:---------|:-----|:---------|:------------| | `name` | `string` | ❌ | Optional controller name for documentation and API introspection | | `path` | `string` | ✅ | Base URL path that prefixes all action paths | | `description` | `string` | ❌ | Optional description for documentation, OpenAPI spec, and MCP Server integration | | `actions` | `Record` | ✅ | Object containing all actions, where keys are action names | The final URL for any action is: `{controller.path}{action.path}`. For example, if a controller has `path: '/users'` and an action has `path: '/:id'`, the final URL will be `/users/:id`. ## 2. Actions: The Heart of Your Business Logic Actions are where the actual work happens. Each Action represents a single API endpoint and is created using either `igniter.query()` for read operations or `igniter.mutation()` for write operations. ### Action Types
Use `igniter.query()` for **read operations** that don't modify data. Typically GET requests. Use `igniter.mutation()` for **write operations** that modify data. POST, PUT, PATCH, DELETE requests.
### Action Anatomy Every action follows the `IgniterAction` interface with these core properties: | Property | Type | Required | Query | Mutation | Description | |:---------|:-----|:---------|:------|:---------|:------------| | `name` | `string` | ❌ | ✅ | ✅ | Optional action name for documentation and MCP Server tool conversion | | `type` | `'query' \| 'mutation'` | ✅ | ✅ | ✅ | Action type (automatically inferred from creation method) | | `path` | `string` | ✅ | ✅ | ✅ | URL path relative to controller, supports parameters like `/:id` | | `method` | `HTTPMethod` | ❌ | ❌ | ✅ | HTTP method (defaults to GET for queries, required for mutations) | | `handler` | `Function` | ✅ | ✅ | ✅ | Async function containing your business logic | | `query` | `StandardSchemaV1` | ❌ | ✅ | ✅ | Schema for validating URL query parameters | | `body` | `StandardSchemaV1` | ❌ | ❌ | ✅ | Schema for validating request body data | | `use` | `IgniterProcedure[]` | ❌ | ✅ | ✅ | Array of procedure middleware to run before handler | | `description` | `string` | ❌ | ✅ | ✅ | Documentation description for OpenAPI docs and MCP Server | | `tags` | `string[]` | ❌ | ✅ | ✅ | Tags for categorization and documentation | | `$Infer` | `TActionInfer` | ✅ | ✅ | ✅ | Internal type inference helper (automatically managed) | ### Complete Action Examples #### Query Action with Full Configuration ```typescript const getUserById = igniter.query({ /** * Optional action name for documentation and MCP Server integration */ name: 'getUserById', /** * URL path - will be combined with controller path * Final URL: /users/:id (if controller path is '/users') */ path: '/:id', /** * HTTP method for this endpoint */ method: 'GET', /** * Query parameters validation using Zod */ query: z.object({ include: z.array(z.enum(['posts', 'profile'])).optional(), fields: z.string().optional(), }), /** * Optional description for API documentation */ description: 'Retrieve a specific user by their ID with optional related data', /** * Tags for categorization and documentation */ tags: ['users', 'read'], /** * Middleware stack - runs before the handler */ use: [authMiddleware, rateLimitMiddleware], /** * The main business logic handler */ handler: async (ctx) => { const { id } = ctx.params; // URL parameters const { include, fields } = ctx.query; // Validated query params const user = await getUserFromDatabase(id, { include, fields: fields?.split(','), }); if (!user) { throw new Error('User not found'); } return { user }; }, }); ``` #### Mutation Action with Body Validation ```typescript const createUser = igniter.mutation({ /** * Optional action name for documentation and MCP Server tool conversion */ name: 'createUser', /** * URL path for creating users */ path: '/', /** * HTTP method for creation */ method: 'POST', /** * Request body validation schema */ body: z.object({ name: z.string().min(2).max(50), email: z.string().email(), age: z.number().int().min(18).optional(), preferences: z.object({ newsletter: z.boolean().default(false), theme: z.enum(['light', 'dark']).default('light'), }).optional(), }), /** * Documentation and metadata */ description: 'Create a new user account with validated data', tags: ['users', 'create'], /** * Middleware for authentication and validation */ use: [authMiddleware, validatePermissions('user:create')], /** * Handler with full type safety */ handler: async (ctx) => { // ctx.body is fully typed based on the schema above const userData = ctx.body; // Check if user already exists const existingUser = await findUserByEmail(userData.email); if (existingUser) { throw new Error('User with this email already exists'); } // Create the user const newUser = await createUserInDatabase({ ...userData, createdAt: new Date(), updatedAt: new Date(), }); // Return the created user (excluding sensitive data) return { user: { id: newUser.id, name: newUser.name, email: newUser.email, createdAt: newUser.createdAt, }, message: 'User created successfully', }; }, }); ``` ### Action Properties Reference Here's a comprehensive breakdown of all available properties for Actions: | Property | Type | Required | Query | Mutation | Description | |:---------|:-----|:---------|:------|:---------|:------------| | `path` | `string` | ✅ | ✅ | ✅ | URL path relative to controller. Supports parameters like `/:id` | | `method` | `HTTPMethod` | ❌ | ❌ | ✅ | HTTP method. Defaults to `GET` for queries, required for mutations | | `handler` | `Function` | ✅ | ✅ | ✅ | Async function containing your business logic | | `query` | `StandardSchemaV1` | ❌ | ✅ | ✅ | Schema for validating URL query parameters | | `body` | `StandardSchemaV1` | ❌ | ❌ | ✅ | Schema for validating request body data | | `use` | `IgniterProcedure[]` | ❌ | ✅ | ✅ | Array of procedure middleware to run before handler | | `name` | `string` | ❌ | ✅ | ✅ | Optional name for the action | | `description` | `string` | ❌ | ✅ | ✅ | Documentation description for API docs | ### HTTP Methods Support ```typescript type HTTPMethod = | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE'; type QueryMethod = 'GET'; // Queries are typically GET requests type MutationMethod = Exclude; // Mutations use other methods ``` ## 3. The Context Object (ctx) Every action handler receives a `ctx` (context) object that provides access to all request data and utilities: ### Context Anatomy The context object (`ctx`) passed to every action handler contains the following properties: | Property | Type | Description | |:---------|:-----|:------------| | `request` | `Object` | Contains all request-related data | | `request.method` | `HTTPMethod` | HTTP method used for the request | | `request.path` | `string` | Action path that was matched | | `request.params` | `Object` | URL parameters inferred from path (e.g., `/:id` → `{ id: string }`) | | `request.headers` | `IgniterHeaders` | Request headers with helper methods | | `request.cookies` | `IgniterCookies` | Request cookies with helper methods | | `request.body` | `T \| undefined` | Validated request body (typed from schema) | | `request.query` | `T \| undefined` | Validated query parameters (typed from schema) | | `context` | `Object` | Enhanced application context with global services | | `response` | `IgniterResponseProcessor` | Response processor for building HTTP responses | | `realtime` | `IgniterRealtimeService` | Service for real-time communication | | `plugins` | `Object` | Type-safe access to registered plugins | ### Context Usage Examples ```typescript // Complete example with all context features const getUserById = igniter.query({ path: '/users/:id', query: z.object({ include: z.array(z.string()).optional(), }), handler: async (ctx) => { // URL Parameters (typed from path) const userId = ctx.request.params.id; // Query Parameters (validated) const includes = ctx.request.query?.include; // Headers const authToken = ctx.request.headers.get('authorization'); // Cookies const sessionId = ctx.request.cookies.get('session-id'); // Set response cookies ctx.response.setCookie('last-visited', new Date().toISOString(), { httpOnly: true, secure: true, maxAge: 86400 }); // Access application context const database = ctx.context.database; // Business logic const user = await database.user.findById(userId); // Return response using response processor return ctx.response.success({ user }); }, }); ``` The `ctx.query` and `ctx.body` objects are fully typed based on your Zod schemas, providing excellent IntelliSense and compile-time safety. ## 4. Deep Dive: Creating a Query Action Let's create a `query` action to fetch a list of users, with support for pagination through query parameters. ```typescript // In src/features/user/controllers/user.controller.ts import { igniter } from '@/igniter'; import { z } from 'zod'; export const userController = igniter.controller({ path: '/users', actions: { /** * An action to list users. * Final Path: GET /users/ */ list: igniter.query({ path: '/', // 1. Define and validate query parameters using Zod. // These are optional and have default values. query: z.object({ page: z.coerce.number().int().positive().optional().default(1), limit: z.coerce.number().int().positive().optional().default(10), }), // 2. The main handler function. handler: async ({ request, context, response }) => { // `request.query` is fully typed by TypeScript as { page: number; limit: number; } // based on the Zod schema above. No manual parsing or validation needed. const { page, limit } = request.query; const skip = (page - 1) * limit; // Use the database client from the global context. const users = await context.database.user.findMany({ take: limit, skip: skip, }); const totalUsers = await context.database.user.count(); // Use the response processor to return a structured, successful response. return response.success({ users, pagination: { page, limit, total: totalUsers, }, }); }, }), }, }); ``` In this example, Igniter.js automatically validates that `page` and `limit` are positive integers. If validation fails, it will return a `400 Bad Request` response with a descriptive error message *before* your handler code is ever executed. ## 4. Deep Dive: Creating a Mutation Action Now, let's create a `mutation` to add a new user to the database. This action will require authentication, which we'll enforce with a procedure. ```typescript // In src/features/user/controllers/user.controller.ts import { igniter } from '@/igniter'; import { z } from 'zod'; import { auth } from '@/features/auth/procedures/auth.procedure'; // Assuming an auth procedure exists export const userController = igniter.controller({ path: '/users', actions: { // ... (list action from above) /** * An action to create a new user. * Final Path: POST /users/ */ create: igniter.mutation({ path: '/', method: 'POST', // 1. Apply the 'auth' procedure to protect this route. // This will run before the handler and can extend the context. use: [auth], // 2. Define and validate the request body using a Zod schema. body: z.object({ name: z.string().min(2), email: z.string().email(), }), // 3. The main handler function. handler: async ({ request, context, response }) => { // `request.body` is fully typed as { name: string; email: string; } const { name, email } = request.body; // `context.user` is available and typed here because the `auth` // procedure added it to the context. const createdBy = context.user; context.logger.info(`User creation initiated by ${createdBy.email}`); const newUser = await context.database.user.create({ data: { name, email }, }); // Use the `created` helper for a 201 Created status code. return response.created(newUser); }, }), }, }); ``` This mutation demonstrates the composability of Igniter.js. The validation, authentication, and business logic are all declared in a clean, readable, and type-safe way. ## The Power of the `ctx` Object The `ctx` object passed to every `handler` is your unified gateway to everything you need for a request. It's an instance of `IgniterActionContext` and contains: - `ctx.request`: Fully-typed request data, including `params`, `query`, `body`, `headers`, and `cookies`. - `ctx.context`: The dynamic application context, containing your global services (like `database`) and any data added by procedures (like `user`). - `ctx.response`: The response processor for building type-safe HTTP responses (`.success()`, `.created()`, `.unauthorized()`, etc.). - `ctx.plugins`: A type-safe entry point for interacting with any registered plugins. By centralizing these concerns, Igniter.js allows you to focus purely on the business logic inside your handler. --- **Next Steps** Now you know how to build the core logic of your API. The next step is to understand the powerful middleware system that makes your code reusable and clean: - **[Procedures (Middleware)](/docs/core-concepts/procedures)** - Learn about middleware - **[Routing](/docs/core-concepts/routing)** - Understand URL routing - **[Validation](/docs/core-concepts/validation)** - Type-safe input validation --- ### Procedures in Igniter.js **Category**: core-concepts **URL**: /docs/core-concepts/procedures # Procedures in Igniter.js Procedures are one of the most powerful and flexible features in Igniter.js, providing a sophisticated middleware system that enables you to create reusable, composable, and type-safe request processing logic. This comprehensive guide will take you through everything you need to know about procedures, from basic concepts to advanced patterns. ## What Are Procedures? In Igniter.js, a **procedure** is a reusable piece of middleware that can be applied to actions or at the builder level. Think of procedures as building blocks that encapsulate common functionality like authentication, logging, rate limiting, input validation, or any custom business logic you need to run before your main handler executes. Unlike traditional middleware that often lacks type safety and composability, Igniter.js procedures are fully type-safe, composable, and provide rich context manipulation capabilities. They can modify the request context, perform early returns, and maintain complete type inference throughout the chain. Procedures operate within the **request lifecycle**, executing before your action handlers and having the ability to: - **Validate and transform input data** - **Authenticate and authorize requests** - **Log request information** - **Implement rate limiting** - **Add custom context data** - **Perform early returns** (like redirects or error responses) - **Access the global application context** (database connections, services, etc.) - **Chain with other procedures** for complex workflows ## Core Concepts ### Procedure Context Every procedure receives an `IgniterProcedureContext` object that contains all the information about the current request: ### Understanding the Procedure Context: A Properties Breakdown To understand the `IgniterProcedureContext` object that every procedure receives, you can expand the section below to see a detailed breakdown of its properties. The incoming request object containing headers, query parameters, body, cookies, and more. The global application context containing database connections, services, configuration, and other shared resources. The response builder object with methods to create successful or error responses. Function to continue to the next middleware or call the final handler. The context is the primary way procedures interact with the request lifecycle, providing a clean and type-safe interface for middleware operations. ## Creating Your First Procedure Let's start with a simple example using the `igniter.procedure` function. We'll create a request logging procedure: ```typescript import { igniter } from '@/igniter'; // Create a simple logging procedure export const requestLogger = igniter.procedure({ name: 'RequestLogger', handler: async ({ request, context, response, next }) => { const startTime = Date.now(); // Use logger from global context if available const logger = context.logger || console; logger.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`); // Continue to the next middleware or handler const result = await next(); const duration = Date.now() - startTime; logger.log(`Request completed in ${duration}ms`); return result; }, }); ``` This procedure: 1. **Logs the incoming request** with timestamp, method, and URL 2. **Calls `next()`** to continue the middleware chain 3. **Logs the completion time** after the request is processed 4. **Returns the result** from the next middleware or handler ### Using the Procedure Once created, you can use this procedure in your controllers: ### Action-Level Procedures ```typescript export const userController = igniter.controller({ name: 'UserController', path: '/users', actions: { getUser: igniter.query({ path: '/:id', // Apply the procedure only to this action use: [requestLogger], handler: ({ request, response }) => { return response.success({ user: { id: request.params.id } }); }, }), }, }); ``` Currently, procedures can only be used at the action level or builder level. Support for router and controller level procedures is planned for future releases. You can track this feature request and vote for it on [GitHub Issue #13](https://github.com/felipebarcelospro/igniter-js/issues/13). ## Best Practices Following established best practices when creating procedures ensures your code remains maintainable, reusable, and type-safe. These guidelines will help you build robust middleware that integrates seamlessly with the Igniter.js ecosystem. Proper procedure design not only improves code quality but also enhances developer experience by making your APIs more predictable and easier to debug. Each practice below addresses common challenges developers face when building production applications. Each procedure should have a single, well-defined responsibility. This follows the Single Responsibility Principle and makes your code more maintainable and testable. ```typescript // Good: Focused on authentication only export const authProcedure = igniter.procedure({ name: 'Authentication', handler: async ({ request, response, next }) => { // Only handle authentication logic }, }); // Good: Focused on logging only export const loggingProcedure = igniter.procedure({ name: 'Logging', handler: async ({ request, response, next }) => { // Only handle logging logic }, }); // Avoid: Mixing multiple concerns export const authAndLoggingProcedure = igniter.procedure({ name: 'AuthAndLogging', handler: async ({ request, response, next }) => { // Don't mix authentication and logging in one procedure }, }); ``` Give your procedures clear, descriptive names that indicate their purpose. This makes your code self-documenting and easier to understand. ```typescript // Good - Clear and descriptive export const jwtAuthenticationProcedure = igniter.procedure({ name: 'JWTAuthentication', handler: async ({ next }) => next(), }); export const requestRateLimitProcedure = igniter.procedure({ name: 'RequestRateLimit', handler: async ({ next }) => next(), }); export const inputValidationProcedure = igniter.procedure({ name: 'InputValidation', handler: async ({ next }) => next(), }); // Avoid - Vague and unclear export const authProc = igniter.procedure({ name: 'Auth', handler: async ({ next }) => next(), }); export const middleware1 = igniter.procedure({ name: 'Middleware1', handler: async ({ next }) => next(), }); ``` Always handle potential errors in your procedures to prevent unhandled exceptions from crashing your application. ```typescript export const safeProcedure = igniter.procedure({ name: 'SafeProcedure', handler: async ({ request, response, next }) => { try { // Your procedure logic const result = await someAsyncOperation(); return next(); } catch (error) { console.error('Procedure error:', error); return response.error({ message: 'Procedure failed', statusCode: 500, }); } }, }); ``` When creating configurable procedures, always use schema validation to ensure type safety and runtime validation. ```typescript const optionsSchema = z.object({ timeout: z.number().min(1000).max(30000).default(5000), retries: z.number().min(0).max(5).default(3), }); export const configurableProcedure = igniter.procedure({ name: 'ConfigurableProcedure', schema: optionsSchema, handler: async ({ options, next }) => { // options are now type-safe and validated }, }); ``` Schema validation ensures type safety for all configuration options. Invalid configurations are caught at runtime with clear error messages. Schema provides sensible defaults for optional configuration parameters. Provide clear documentation for your procedures, especially their configuration options, to help other developers understand and use them effectively. ```typescript /** * Rate limiting procedure that restricts the number of requests per time window. * * @example * ```typescript * use: [ * rateLimitProcedure({ * maxRequests: 100, * windowMs: 60000, // 1 minute * message: 'Too many requests' * }) * ] * ``` */ export const rateLimitProcedure = igniter.procedure({ name: 'RateLimit', schema: z.object({ /** Maximum number of requests allowed in the time window */ maxRequests: z.number().min(1).default(100), /** Time window in milliseconds */ windowMs: z.number().min(1000).default(60000), /** Error message to return when rate limit is exceeded */ message: z.string().default('Too many requests'), }), handler: async ({ options, request, response, next }) => { // Implementation }, }); ``` Use JSDoc comments to describe the procedure's purpose and usage. Provide clear examples showing how to configure and use the procedure. Document each configuration parameter with inline comments. ## Conclusion Procedures are a fundamental building block of Igniter.js applications, providing a powerful and flexible way to implement cross-cutting concerns in your API using the `igniter.procedure` function. By understanding how to create, compose, and use procedures effectively, you can build more maintainable, reusable, and type-safe applications. ### Key Takeaways Use procedures for authentication, logging, validation, and other shared functionality across your application. Leverage the type-safe context system for secure data sharing between procedures and handlers. Compose procedures to build complex middleware chains with predictable execution order. Follow the single responsibility principle to create maintainable and testable code. Keep procedures focused, use descriptive names, and implement proper error handling. With this comprehensive understanding of procedures, you're ready to build sophisticated, production-ready APIs with Igniter.js. Explore [Controllers and Actions](/docs/core-concepts/controllers) and [Request and Response Handling](/docs/core-concepts/request-response) to complete your knowledge. --- ### Routing: Assembling Your API **Category**: core-concepts **URL**: /docs/core-concepts/routing # Routing: Assembling Your API The **Igniter.js Router** is the final and most crucial step in the backend configuration process. Its purpose is to take all the individual `Controllers` you've built and assemble them into a single, unified, and fully-routable API. The `AppRouter`, which you create in `src/igniter.router.ts`, becomes the single source of truth for your entire API's structure. It's not just a request handler; it's a complete, type-safe definition of every endpoint, which is later used to power the end-to-end type safety of the client. ## 1. Creating the Application Router You create your application's router using the `igniter.router()` factory function. This is typically done once in `src/igniter.router.ts`. **Example: A typical `igniter.router.ts` file** ```typescript // src/igniter.router.ts import { igniter } from '@/igniter'; // 1. Import all the controllers you've created import { userController } from '@/features/user/controllers/user.controller'; import { postController } from '@/features/post/controllers/post.controller'; /** * The main application router. * It combines all controllers into a single API definition. */ export const AppRouter = igniter.router({ /** * The collection of all controllers registered with this router. */ controllers: { // 2. Register your controllers here users: userController, posts: postController, }, }); /** * We export the *type* of the AppRouter. * This is crucial for providing type safety to the client without * bundling any server-side code. */ export type AppRouter = typeof AppRouter; ``` ## 2. Router Configuration Explained Let's break down the configuration options for `igniter.router()`: ### `controllers` This is the most important property. It's an object where you register all the feature controllers that make up your API. - **The `key` is important:** The key you assign to each controller (e.g., `users`, `posts`) directly maps to the namespace used on the type-safe client. Registering `users: userController` is what enables you to later call `api.users.list.useQuery()` on the frontend. - **The `value` is the controller instance** created with `igniter.controller()`. ## 3. Integrating the Router with a Web Framework The `AppRouter` object exposes a `handler` function that is framework-agnostic. It's designed to work with the standard Web `Request` and `Response` objects, making it compatible with any modern Node.js web framework. ### Integration with Next.js (Recommended) Igniter.js provides a dedicated adapter for Next.js App Router to make integration seamless. Create a catch-all route handler at `src/app/api/v1/[[...all]]/route.ts`: ```typescript // src/app/api/v1/[[...all]]/route.ts import { AppRouter } from '@/igniter.router'; import { nextRouteHandlerAdapter } from '@igniter-js/core/adapters'; /** * The adapter takes your AppRouter and returns an object containing * handlers for each HTTP method (GET, POST, etc.). * Next.js will automatically call the correct handler based on the * incoming request's method. */ export const { GET, POST, PUT, DELETE, PATCH } = nextRouteHandlerAdapter(AppRouter); ``` This single file is all you need to connect your entire Igniter.js API to your Next.js application. ### Integration with Other Frameworks (e.g., Express) You can easily create your own adapter for other frameworks like Express or Hono. ```typescript // Example for an Express server import express from 'express'; import { AppRouter } from '@/igniter.router'; import { createExpressAdapter } from './my-express-adapter'; // A simple custom adapter const app = express(); // Use the handler for all routes matching the base path app.use('/api/v1/*', createExpressAdapter(AppRouter.handler)); app.listen(3000, () => { console.log('Server is running on http://localhost:3000'); }); ``` ## 4. The Router's Role in End-to-End Type Safety The `AppRouter` object is more than just a request handler. It is a deeply-typed representation of your entire API. This static object contains all the information about every controller, action, path, input schema, and output type. When you create your **Type-Safe Client**, you will import the *type* of `AppRouter` (`type AppRouter = typeof AppRouter`). The client uses this type to generate a fully-typed SDK for your frontend, ensuring that your backend and frontend are always perfectly in sync. --- **Next Steps** Congratulations! You have now learned how to build a complete, fully-functional backend with Igniter.js. You can define the application's core with the Builder, manage dependencies with Context, write business logic in Controllers and Actions, create reusable middleware with Procedures, and finally, assemble everything with the Router. The next logical step is to learn how to consume this powerful, type-safe API from a frontend application: - **[Validation](/docs/core-concepts/validation)** - Type-safe input validation - **[Client-Side Integration](/docs/client-side)** - Connect your frontend - **[Advanced Features](/docs/advanced-features)** - Explore queues, store, and more --- ### The Igniter Builder: Your Application's Foundation **Category**: core-concepts **URL**: /docs/core-concepts/igniter-builder # The Igniter Builder: Your Application's Foundation Every Igniter.js application begins with the **Igniter Builder**. It is a fluent, chainable API designed to guide you through the process of composing and configuring your application's core components in a structured and fully type-safe manner. Think of the builder as the master blueprint for your entire backend. It's where you define the application's context, integrate services like logging and caching, enable advanced features like background jobs, and extend functionality with plugins. ## Philosophy The Igniter Builder is designed around five core principles: 1. **Guided Configuration**: Each method guides you through the setup process with clear, type-safe APIs. 2. **Compile-time Safety**: All configurations are validated at compile time, catching errors before they reach production. 3. **Explicitness**: Every configuration is explicit and intentional, making your application's behavior predictable. 4. **Modularity**: Each adapter and service can be configured independently, allowing for flexible architectures. 5. **Testability**: The builder pattern makes it easy to create different configurations for testing, development, and production. ## How It Works The Igniter Builder uses a fluent interface pattern where each method returns a new builder instance with updated type information. This ensures that TypeScript can provide accurate autocompletion and type checking throughout your configuration process. ```typescript const igniter = Igniter .context() .middleware([authMiddleware, loggingMiddleware]) .store(redisAdapter) .logger(consoleLogger) .telemetry(openTelemetryProvider) .create(); ``` ## Configuration Methods ```typescript type AppContext = { user: User | null; db: Database; config: AppConfig; }; const igniter = Igniter.context(); ``` **Key Points:** - The context type is used for type inference across all actions and procedures - Context can be populated by middleware or provided directly in actions - Must be defined before other configuration methods for proper type inference - Supports both static types and dynamic context callbacks ```typescript import { authMiddleware, loggingMiddleware } from './middleware'; const igniter = Igniter .context() .middleware([authMiddleware, loggingMiddleware]); ``` **Key Points:** - Middleware is executed in the order specified in the array - Global middleware applies to all controllers and actions - Each middleware can modify the context or handle errors - Type-safe middleware composition with full TypeScript inference ```typescript const igniter = Igniter .context() .config({ baseURL: 'https://api.example.com', basePATH: '/v1' }); ``` **Available Options:** - `baseURL` - The base URL for your API - `basePATH` - The base path prefix for all routes ```typescript import { RedisAdapter } from '@igniter-js/adapter-redis'; const store = new RedisAdapter({ host: 'localhost', port: 6379, db: 0 }); const igniter = Igniter .context() .store(store); ``` **Supported Adapters:** - `@igniter-js/adapter-redis` - Redis-based storage with pub/sub support - Custom adapters implementing `IgniterStoreAdapter` interface **Store Features:** - Key-value storage with TTL support - Pub/Sub messaging for real-time features - Automatic serialization/deserialization - Type-safe operations ```typescript import { ConsoleLogger } from '@igniter-js/logger-console'; const logger = new ConsoleLogger({ level: 'info', format: 'json' }); const igniter = Igniter .context() .logger(logger); ``` **Logger Interface:** - `info(message, meta?)` - Log informational messages - `warn(message, meta?)` - Log warning messages - `error(message, meta?)` - Log error messages - `debug(message, meta?)` - Log debug messages **Custom Loggers:** Implement the `IgniterLogger` interface to create custom logging solutions. ```typescript import { BullMQAdapter } from '@igniter-js/adapter-bullmq'; const jobs = new BullMQAdapter({ connection: { host: 'localhost', port: 6379 }, defaultJobOptions: { removeOnComplete: 10, removeOnFail: 5 } }); const igniter = Igniter .context() .jobs(jobs); ``` **Supported Adapters:** - `@igniter-js/adapter-bullmq` - BullMQ integration with Redis **Job Features:** - Type-safe job definitions and handlers - Delayed and scheduled job execution - Job retry mechanisms and error handling - Queue monitoring and management ```typescript import { OpenTelemetryProvider } from '@igniter-js/adapter-opentelemetry'; const telemetry = new OpenTelemetryProvider({ serviceName: 'my-api', serviceVersion: '1.0.0', exporters: { traces: 'jaeger', metrics: 'prometheus' } }); const igniter = Igniter .context() .telemetry(telemetry); ``` **Supported Providers:** - `@igniter-js/adapter-opentelemetry` - OpenTelemetry integration **Telemetry Features:** - Distributed tracing across requests - Custom metrics and counters - Performance monitoring - Error tracking and alerting - Integration with popular observability platforms ```typescript import { AuthPlugin } from '@igniter-js/plugin-auth'; import { CachePlugin } from '@igniter-js/plugin-cache'; const igniter = Igniter .context() .plugins({ auth: new AuthPlugin({ secret: process.env.JWT_SECRET, expiresIn: '7d' }), cache: new CachePlugin({ ttl: 3600 }) }); ``` **Plugin System:** - Plugins can extend context, add middleware, and provide utilities - Type-safe plugin configuration and usage - Plugins are accessible throughout your application via the context - Support for plugin lifecycle hooks and resource management ```typescript const igniter = Igniter .context() .docs({ openapi: '3.0.0', info: { title: 'My API', version: '1.0.0', description: 'A comprehensive API built with Igniter.js' }, servers: [ { url: 'https://api.example.com/v1', description: 'Production server' }, { url: 'http://localhost:3000/v1', description: 'Development server' } ], securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } }, playground: { enabled: true, route: '/docs', security: (request) => { // Custom security check for playground access return process.env.NODE_ENV === 'development'; } } }); ``` **Documentation Features:** - Automatic OpenAPI specification generation - Interactive API playground (Scalar) - Multiple server configurations - Security scheme definitions - Custom playground access control Creates the final Igniter instance with all configured adapters and services. ```typescript const igniter = Igniter .context() .middleware([authMiddleware]) .store(redisAdapter) .logger(consoleLogger) .jobs(bullmqAdapter) .telemetry(openTelemetryProvider) .create(); ``` **Returns an object with:** - `controller` - Function to create controllers - `query` - Function to create query actions (GET requests) - `mutation` - Function to create mutation actions (POST, PUT, DELETE, PATCH) - `procedure` - Function to create reusable procedures and middleware - `router` - Function to create nested routers - `logger` - Configured logger instance - `store` - Configured store instance - `jobs` - Configured jobs instance **Type Safety:** All returned functions are fully typed based on your configuration, providing complete TypeScript inference throughout your application. ## What's Next? Now that you have your Igniter instance configured, you can start building your application: - **[Context](/docs/core-concepts/context)** - Learn how to work with application context - **[Controllers & Actions](/docs/core-concepts/controllers-and-actions)** - Create your API endpoints - **[Procedures](/docs/core-concepts/procedures)** - Build reusable middleware and logic --- ### Validation: Ensuring Data Integrity and Business Rules **Category**: core-concepts **URL**: /docs/core-concepts/validation # Validation: Ensuring Data Integrity and Business Rules In any robust API, validation is a critical, non-negotiable step. It protects your application from invalid data, prevents unexpected errors, and enforces your business rules. Igniter.js treats validation as a first-class citizen and provides a powerful, two-layer approach to handle it cleanly and efficiently. 1. **Schema Validation**: Validating the **shape and type** of incoming data (`body`, `query parameters`). 2. **Business Logic Validation**: Validating **runtime conditions and business rules** (e.g., "does this user have permission?"). ## 1. Schema Validation with Zod For validating the structure of incoming data, Igniter.js has built-in, first-class support for **Zod**. You define a Zod schema for your action's `body` or `query` properties, and Igniter.js handles the rest automatically. **How it works:** Before your action's `handler` is ever executed, Igniter.js intercepts the incoming request and validates its body and query parameters against the Zod schemas you provided. - **On Success:** The data is guaranteed to be valid. TypeScript correctly infers the types, and the parsed, type-safe data is made available to you in `request.body` and `request.query`. - **On Failure:** The validation fails. Igniter.js immediately halts the request and sends a detailed `400 Bad Request` response to the client, specifying which fields are invalid and why. Your handler is never called. ### Example: Validating a Mutation Body Let's create a `mutation` to create a new product, with strict validation rules for the request body. ```typescript import { igniter } from '@/igniter'; import { z } from 'zod'; export const productController = igniter.controller({ path: '/products', actions: { create: igniter.mutation({ path: '/', method: 'POST', // Define the validation schema for the request body body: z.object({ name: z.string().min(3, "Name must be at least 3 characters long."), price: z.number().positive("Price must be a positive number."), category: z.enum(['electronics', 'books', 'clothing']), stock: z.number().int().nonnegative().default(0), }), handler: async ({ request, response, context }) => { // If the code reaches here, the data is valid. // `request.body` is fully typed as: // { name: string; price: number; category: "electronics" | "books" | "clothing"; stock: number; } const { name, price, category, stock } = request.body; const product = await context.database.product.create({ data: { name, price, category, stock } }); return response.created(product); }, }), }, }); ``` With this setup, you never have to write `if (!body.name)` or `if (typeof body.price !== 'number')` inside your handler. The framework guarantees data integrity before your logic runs. ## 2. Business Logic Validation with `Ensure` Schema validation is perfect for checking data shapes, but what about rules that depend on your application's state? For example: - Does the user with this ID actually exist in the database? - Does the current user have the 'admin' role? - Is the product we're trying to add to the cart in stock? This is where the **Igniter.js Ensure** service comes in. `Ensure` is a utility that provides a clean, declarative, and type-safe way to assert business rules, replacing messy `if/throw` blocks. ### The Problem: Repetitive `if/throw` Without a utility like `Ensure`, your code can become cluttered with repetitive validation logic: ```typescript // The "old" way with if/throw handler: async ({ request, context, response }) => { const { productId } = request.body; const product = await context.database.product.findUnique({ where: { id: productId }, }); // Repetitive check if (!product) { // Manually throwing an error return response.notFound({ message: `Product with ID ${productId} not found.` }); } const currentUser = context.auth.user; // Another repetitive check if (currentUser.role !== 'admin') { return response.forbidden({ message: 'You do not have permission.' }); } // Now, TypeScript still thinks `product` can be `null` here without extra work. // ... rest of the logic } ``` ### The Solution: Declarative Assertions with `Ensure` The `Ensure` service replaces these blocks with single, readable lines. It also provides powerful type-narrowing. ```typescript // The "new" way with Ensure handler: async ({ request, context, response }) => { const { productId } = request.body; const product = await context.database.product.findUnique({ where: { id: productId }, }); // 1. Assert that the product must exist. // If not, it throws a formatted Not Found error automatically. context.$plugins.ensure.toBeDefined(product, `Product with ID ${productId} not found.`); // After this line, TypeScript knows `product` CANNOT be null. Its type is narrowed. // 2. Assert a boolean condition is true. context.$plugins.ensure.toBeTrue( context.auth.user.role === 'admin', 'You do not have permission to perform this action.' // This throws a Forbidden error. ); // Your business logic is clean and only runs if all assertions pass. // ... rest of the logic } ``` *Note: `Ensure` is typically added as a plugin to be available on the context.* ### Key `Ensure` Methods | Method | Description | | :---------------------------- | :-------------------------------------------------------------------------------------------------------- | | `toBeDefined(value, msg)` | Ensures a value is not `null` or `undefined`. Narrows the type. | | `toBeNotEmpty(value, msg)` | Ensures a string is not empty, `null`, or `undefined`. | | `toBeTrue(condition, msg)` | Ensures a boolean condition is `true`. | | `toBeFalse(condition, msg)` | Ensures a boolean condition is `false`. | | `toBeOneOf(value, array, msg)`| Checks if a value is present in a given array of options. | | `toMatchPattern(val, regex, msg)`| Validates a string against a regular expression. | ### When to Use Which Validation - **Use Zod Schemas for:** The **shape and type** of data sent by the client. This is your first line of defense at the entry point of your API. - **Use `Ensure` for:** **Business rules and runtime conditions** that require access to your application's state (database, user session, etc.) inside your handlers. By combining these two layers, you can build extremely robust, readable, and maintainable APIs with Igniter.js. --- ## Next Steps Now that you understand validation in Igniter.js, you can explore: - [Client-Side Integration](/docs/client-side) - Learn how to consume your validated APIs from the frontend - [Advanced Features](/docs/advanced-features) - Discover more powerful features of Igniter.js - [CLI & Tooling](/docs/cli-and-tooling) - Explore the development tools available --- ### Client-Side: Fetching Data with `useQuery` **Category**: client-side **URL**: /docs/client-side/use-query # Client-Side: Fetching Data with `useQuery` The `useQuery` hook is the primary tool for fetching data from your Igniter.js backend in a client-side React component. It is a **completely custom implementation** built from scratch specifically for Igniter.js, providing a familiar and powerful API with a crucial advantage: it's **end-to-end type-safe**. This means the parameters you pass to the hook and the data it returns are all automatically typed based on your backend's `AppRouter` definition. ## 1. Basic Usage To use the hook, you access it through the `api` client you created, following the path to your desired query action: `api...useQuery()`. **Example: Fetching a list of posts** ```tsx // app/components/PostsList.tsx 'use client'; import { api } from '@/igniter.client'; function PostsList() { // 1. Call the useQuery hook const postsQuery = api.posts.list.useQuery(); // 2. Handle the loading state if (postsQuery.isLoading) { return
Loading posts...
; } // 3. Handle the error state if (postsQuery.isError) { return
Error fetching posts: {postsQuery.error.message}
; } // 4. Render the success state // `postsQuery.data` is fully typed based on your backend action's return value. return (
    {postsQuery.data?.posts.map((post) => (
  • {post.title}
  • ))}
); } ``` ## 2. Passing Parameters Most queries require parameters to fetch specific data. The `useQuery` hook accepts a configuration object where you can provide `params` (for URL path parameters) and `query` (for URL query parameters). ### `query` Parameters (for filtering, pagination) If your backend action defines a `query` schema, you can pass matching data here. **Backend Action:** ```typescript list: igniter.query({ path: '/', query: z.object({ page: z.number() }), // ... }) ``` **Frontend `useQuery` call:** ```tsx // Pass the page number as a query parameter. const postsQuery = api.posts.list.useQuery({ query: { page: 2 }, // This is type-checked! }); ``` ### `params` (for dynamic routes) If your backend action has a dynamic path, you provide the values in the `params` object. **Backend Action:** ```typescript getById: igniter.query({ path: '/:postId', // Dynamic segment // ... }) ``` **Frontend `useQuery` call:** ```tsx const postQuery = api.posts.getById.useQuery({ // Provide the value for the ':postId' dynamic segment. params: { postId: '123' }, // Also type-checked! }); ``` ## 3. Key Return Values The `useQuery` hook returns an object with a rich set of properties to manage the entire lifecycle of a data-fetching request. | Property | Description | | :------------ | :-------------------------------------------------------------------------------------------------------------- | | `data` | The data returned from a successful query. It will be `undefined` until the fetch succeeds. | | `variables` | The parameters (`query`, `params`) that were used for the most recent query execution. | | `isLoading` | A boolean that is `true` only during the very first fetch for a query. | | `isFetching` | A boolean that is `true` whenever a request is in-flight (including initial load and subsequent refetches). | | `isSuccess` | A boolean that is `true` if the query has completed successfully. | | `isError` | A boolean that is `true` if the query has failed. | | `error` | If `isError` is true, this property will contain the error object. | | `refetch` | A function you can call to manually trigger a refetch of the query. | | `status` | A string representing the query's state: `'loading'`, `'error'`, or `'success'`. | ## 4. Configuration Options You can customize the behavior of `useQuery` by passing an options object. Here are some of the most common options: ### `enabled` A boolean to conditionally enable or disable a query. If `false`, the query will not run automatically. This is useful for dependent queries (e.g., fetching a user's profile only after you have their ID). ```tsx const session = useUserSession(); const userProfileQuery = api.users.getProfile.useQuery({ // Only run this query if we have a valid session and user ID. enabled: !!session.isAuthenticated && !!session.userId, }); ``` ### `staleTime` The time in milliseconds that query data is considered "fresh". As long as data is fresh, it will be served from the cache without a network request. After `staleTime` has passed, the data is considered "stale" and will be refetched in the background on the next render. - **Type:** `number` - **Default:** `0` (data is considered stale immediately) ```tsx // Consider this data fresh for 5 minutes (300,000 ms) const query = api.users.list.useQuery({ staleTime: 1000 * 60 * 5, }); ``` ### `refetchInterval` If set to a number, the query will automatically refetch at that interval in milliseconds. This is useful for polling data that changes frequently. - **Type:** `number | false` - **Default:** `false` ```tsx // Refetch this data every 30 seconds const query = api.system.status.useQuery({ refetchInterval: 30000, }); ``` ### `refetchOnWindowFocus` If `true`, the query will automatically refetch whenever the browser window regains focus. This is a great way to ensure data is up-to-date when a user returns to your application. - **Type:** `boolean` - **Default:** `true` --- ## 5. Lifecycle Callbacks You can execute side effects based on the query's result using callback functions. | Callback | Description | | :-------------- | :------------------------------------------------------------------------ | | `onSuccess(data)` | A function that is called if the query succeeds. It receives the `data`. | | `onError(error)` | A function that is called if the query fails. It receives the `error`. | | `onSettled(data, error)` | A function that is called when the query finishes, whether it succeeded or failed. | **Example:** ```tsx const userQuery = api.users.getById.useQuery({ params: { id: userId }, onSuccess: (data) => { // This runs only on success console.log(`Successfully fetched user: ${data.user.name}`); // You could trigger another action here, like sending an analytics event. }, onError: (error) => { // This runs only on failure console.error(`Failed to fetch user: ${error.message}`); // You could show a toast notification here. } }); ``` By mastering these options, you can fine-tune your data-fetching logic to create a highly performant and responsive user experience. --- ## Next Steps Now that you know how to fetch data, the next step is to learn how to modify it: - [useMutation](/docs/client-side/use-mutation) - Learn how to modify data with mutations - [useRealtime](/docs/client-side/use-realtime) - Explore real-time features and data streams - [API Client](/docs/client-side/api-client) - Understand the type-safe client architecture --- ### Client-Side: Modifying Data with `useMutation` **Category**: client-side **URL**: /docs/client-side/use-mutation # Client-Side: Modifying Data with `useMutation` While `useQuery` is for fetching data, **`useMutation`** is the hook you'll use for any action that modifies data on the server. This includes creating, updating, and deleting resources, corresponding to backend actions that use `POST`, `PUT`, `PATCH`, or `DELETE` methods. The `useMutation` hook, accessed via `api...useMutation()`, provides a simple and declarative API to handle the entire lifecycle of a data modification, from optimistic updates to error handling and cache invalidation. ## 1. Basic Usage A `useMutation` hook provides you with a `mutate` function (or `mutateAsync` for a promise-based version) that you can call to trigger the mutation. **Example: A "Create Post" Form** ```tsx // app/components/CreatePostForm.tsx 'use client'; import { api } from '@/igniter.client'; import { useState } from 'react'; function CreatePostForm() { const [title, setTitle] = useState(''); const [content, setContent] = useState(''); // 1. Initialize the mutation hook const createPostMutation = api.posts.create.useMutation(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // 2. Call the `mutate` function with the required input // The `body` object is fully type-safe based on your backend Zod schema. createPostMutation.mutate({ body: { title, content }, }); }; return (
{/* ... form inputs for title and content ... */} {/* 3. Use the `isLoading` state for UI feedback */} {createPostMutation.isError && (

Error: {createPostMutation.error.message}

)}
); } ``` ## 2. Key Return Values The `useMutation` hook returns an object with properties to manage the mutation's state: | Property | Description | | :---------- | :------------------------------------------------------------------------------------------------------- | | `mutate` | A function to trigger the mutation. It takes one argument: an object with `body`, `query`, or `params`. | | `data` | The data returned from your backend action handler upon a successful mutation. It is `undefined` until the mutation succeeds. | | `variables` | The variables (`body`, `query`, `params`) passed to the most recent `mutate` call. It is `undefined` until the mutation is called. | | `isLoading` | A boolean that is `true` while the mutation is in flight. | | `isSuccess` | A boolean that is `true` if the mutation completed successfully. | | `isError` | A boolean that is `true` if the mutation failed. | | `error` | If `isError` is true, this property will contain the error object. | | `retry` | A function to re-run the last mutation with the same variables. | | `status` | A string representing the mutation's state: `'loading'`, `'error'`, or `'success'`. | --- ## 3. Lifecycle Callbacks To handle side effects like showing notifications or redirecting the user, `useMutation` accepts an options object with callback functions. | Callback | Description | | :------------------- | :------------------------------------------------------------------------------------------------------ | | `onSuccess(data)` | Runs if the mutation is successful. Receives the `data` from the server. | | `onError(error)` | Runs if the mutation fails. Receives the `error` object. | | `onSettled(data, error)` | Runs when the mutation finishes, regardless of whether it succeeded or failed. Receives data and error. | **Example: Showing Notifications on Success or Failure** ```tsx const createPostMutation = api.posts.create.useMutation({ onSuccess: (data) => { // `data` is the response from the backend action console.log(`Successfully created post with ID: ${data.post.id}`); // showSuccessToast('Post created!'); }, onError: (error) => { console.error(`Failed to create post: ${error.message}`); // showErrorToast(error.message); }, onSettled: () => { // This runs after either onSuccess or onError console.log('Mutation has settled.'); } }); ``` --- ## 4. The Most Important Pattern: Cache Invalidation After a mutation successfully modifies data on the server, your client-side cache is now out-of-date. For example, after creating a new post, your list of posts is incomplete. The best practice is to **invalidate** the relevant queries in the `onSuccess` callback. This tells Igniter.js to automatically refetch that data, ensuring your UI always reflects the latest state. To do this, you use the `useQueryClient` hook. **Example: Refetching the Post List After Creation** ```tsx 'use client'; import { api, useQueryClient } from '@/igniter.client'; function CreatePostForm() { // 1. Get an instance of the query client const queryClient = useQueryClient(); const createPostMutation = api.posts.create.useMutation({ onSuccess: () => { console.log('Post created, invalidating post list...'); // 2. Invalidate the 'posts.list' query. // This will cause any component using `api.posts.list.useQuery()` to refetch. queryClient.invalidate(['posts.list']); }, // ... onError handling }); const handleSubmit = (e) => { // ... createPostMutation.mutate({ body: { ... } }); }; // ... rest of the form } ``` This pattern is fundamental to building modern, reactive web applications. It ensures that the user's actions are immediately reflected in the UI without needing complex manual state management. If you are using **Igniter.js Realtime**, you can often skip manual invalidation and use server-side `.revalidate()` for a more powerful, automated approach. --- ## Next Steps - [useRealtime](/docs/client-side/use-realtime) - Learn about real-time features and automatic cache invalidation - [useQuery](/docs/client-side/use-query) - Understand how to fetch data with type safety - [API Client](/docs/client-side/api-client) - Explore the type-safe client architecture --- ### Client-Side: Subscribing with `useRealtime` **Category**: client-side **URL**: /docs/client-side/use-realtime # Client-Side: Subscribing with `useRealtime` For features that require a persistent, real-time flow of data from the server—like live notifications, chat applications, or activity feeds—Igniter.js provides the **`useRealtime`** hook. Unlike `useQuery`, which fetches data once and then caches it, `useRealtime` establishes a continuous subscription to a specific backend channel. It listens for messages pushed by the server and provides callbacks to react to them as they arrive. ## Backend Prerequisite The `useRealtime` hook can only be used with backend `query` actions that have been explicitly marked as streamable by setting the `stream: true` option. ```typescript // In your backend controller export const notificationController = igniter.controller({ path: '/notifications', actions: { stream: igniter.query({ path: '/stream', // This property is required for useRealtime to work. stream: true, handler: async ({ response }) => { // This handler runs once when the client first connects. return response.success({ status: 'Connected' }); } }) } }); ``` This creates a dedicated real-time channel named `notifications.stream`. --- ## 1. Basic Usage To subscribe to a stream, you access the hook via your `api` client and provide an `onMessage` callback to handle incoming data. **Example: A Live Notification Feed** ```tsx 'use client'; import { api } from '@/igniter.client'; import { useState, useEffect } from 'react'; interface Notification { id: string; text: string; } function NotificationFeed() { const [notifications, setNotifications] = useState([]); // 1. Subscribe to the stream api.notifications.stream.useRealtime({ // 2. This callback runs for every message sent by the server onMessage: (newNotification: Notification) => { console.log('New notification received:', newNotification); setNotifications((currentNotifications) => [ newNotification, ...currentNotifications, ]); }, }); return (

Live Notifications

    {notifications.map((notif) => (
  • {notif.text}
  • ))}
); } ``` --- ## 2. Configuration Options (`RealtimeActionCallerOptions`) You can customize the behavior of the stream by passing an options object to the `useRealtime` hook. | Option | Description | | :--------------------- | :------------------------------------------------------------------------------------------------ | | `onMessage` | **(Required)** A callback function that runs every time a new message is received from the server. | | `onConnect` | A callback that runs once when the stream successfully connects to the server. | | `onDisconnect` | A callback that runs if the stream connection is lost. | | `onError` | A callback that runs if an error occurs with the connection. | | `initialData` | Provides an initial value for the `data` state before the first message arrives. | | `initialParams` | An object with `query` or `params` to initialize the stream connection. | | `autoReconnect` | If `true`, the client will automatically attempt to reconnect if the connection drops. (Default: `true`) | | `maxReconnectAttempts` | The maximum number of times to try reconnecting. | | `reconnectDelay` | The delay in milliseconds between reconnection attempts. | **Example with more options:** ```tsx api.activity.feed.useRealtime({ initialParams: { query: { filter: 'all' } }, onConnect: () => { showToast('Connected to activity feed!'); }, onMessage: (activity) => { addActivityToList(activity); }, onError: (error) => { console.error('Stream connection error:', error); showErrorToast('Could not connect to live feed.'); } }); ``` --- ## 3. Return Values (`RealtimeActionCallerResult`) The `useRealtime` hook returns an object with properties and functions to interact with the stream's state. | Property | Description | | :--------------- | :------------------------------------------------------------------------------ | | `data` | The most recent message received from the stream. | | `isConnected` | A boolean indicating if the stream is currently connected. | | `isReconnecting` | A boolean indicating if the client is currently attempting to reconnect. | | `error` | The last error object, if any. | | `disconnect()` | A function to manually close the stream connection. | | `reconnect()` | A function to manually attempt to reconnect a disconnected stream. | **Example: Displaying connection status and manual controls** ```tsx function StreamControls() { const { isConnected, isReconnecting, disconnect, reconnect } = api.logs.stream.useRealtime({ onMessage: (log) => console.log(log), }); if (isReconnecting) { return

Connection lost. Reconnecting...

; } return (

Stream Status: {isConnected ? 'Connected' : 'Disconnected'}

); } ``` By combining these options and return values, `useRealtime` provides a complete and robust solution for building rich, real-time user experiences with Igniter.js. --- ## Next Steps - [API Client](/docs/client-side/api-client) - Understand the type-safe client architecture - [IgniterProvider](/docs/client-side/igniter-provider) - Learn about the provider setup for real-time features - [Realtime](/docs/advanced-features/realtime) - Explore advanced real-time features on the backend --- ### Client-Side: The `` **Category**: client-side **URL**: /docs/client-side/igniter-provider # Client-Side: The `` The `` is the root component that powers the entire Igniter.js client-side experience. It is a mandatory wrapper that must be placed at the root of your React application tree. Its primary responsibilities are: 1. **Query Cache Management:** It initializes and provides the cache for all API queries. The client-side implementation is a **completely custom solution** built from scratch specifically for Igniter.js. This enables automatic caching, re-fetching, and state management for hooks like `useQuery` and `useMutation`. 2. **Realtime Connection:** It manages the persistent Server-Sent Events (SSE) connection to your backend, which is essential for `Igniter.js Realtime` features like automatic revalidation and custom data streams. 3. **Client Context:** It holds the client-side context, such as the current user's session, making it available for features like scoped real-time updates. > **Important:** None of the client-side hooks (`useQuery`, `useMutation`, `useStream`) will work unless they are descendants of an ``. ## 1. Basic Setup The provider should be placed as high up in your component tree as possible, typically in your root layout file. In a Next.js App Router application, this is often done in a dedicated `app/providers.tsx` file. **Example: Setting up the provider** ```tsx // app/providers.tsx 'use client'; import { IgniterProvider } from '@igniter-js/core/client'; import type { PropsWithChildren } from 'react'; export function Providers({ children }: PropsWithChildren) { return ( {children} ); } ``` Then, use this `Providers` component in your root `layout.tsx`: ```tsx // app/layout.tsx import { Providers } from './providers'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` --- ## 2. Configuration Props The `` accepts several optional props to configure its behavior, especially for real-time features. ### `enableRealtime` Controls whether the real-time SSE client is enabled. It defaults to `true`. - **Type:** `boolean` - **Default:** `true` ```tsx {/* Realtime features like .revalidate() and useStream will be disabled. */} ``` ### `autoReconnect` If the SSE connection is lost, this prop determines whether the client will automatically try to reconnect. - **Type:** `boolean` - **Default:** `true` ```tsx {/* The client will not attempt to reconnect if the connection drops. */} ``` --- ## 3. Scoped Realtime with `getContext` and `getScopes` For **scoped revalidation** to work, you must configure the `getContext` and `getScopes` props. This tells the provider which "channels" or "topics" the current client is interested in. ### `getContext` A function that returns an object representing the client-side context. This is typically where you provide information about the currently logged-in user. - **Type:** `() => TContext` - **Purpose:** To provide data that can be used by other provider props, like `getScopes`. ### `getScopes` A function that receives the client context (from `getContext`) and returns an array of string identifiers. These strings are the "scopes" that this client will subscribe to for real-time updates. - **Type:** `(context: TContext) => string[]` - **Purpose:** To subscribe the client to specific real-time channels. **Example: A complete setup for a logged-in user** ```tsx // app/providers.tsx import { IgniterProvider } from '@igniter-js/core/client'; export function Providers({ children }: { children: React.ReactNode }) { return ( { 'use server' const session = await getSession() if (!session) { // If no user is logged in, subscribe to no specific scopes. return []; } // Subscribe the client to a scope for their user ID and for each of their roles. return [ `user:${session.user.id}`, ...session.user.roles.map(role => `role:${role}`) ]; }} > {children} ); } ``` With this configuration, the client is now set up to receive targeted real-time updates. When a backend mutation calls `.revalidate(['some-key'], (ctx) => ['user:123'])`, only the client whose user ID is `123` will receive the revalidation event. --- ## Next Steps Now that your application is wrapped in the provider, you're ready to start fetching and modifying data: - [useQuery](/docs/client-side/use-query) - Learn how to fetch data with type safety - [useMutation](/docs/client-side/use-mutation) - Discover how to perform mutations safely - [useRealtime](/docs/client-side/use-realtime) - Explore real-time features and data streams --- ### Client-Side: The Type-Safe API Client **Category**: client-side **URL**: /docs/client-side/api-client # Client-Side: The Type-Safe API Client The **Igniter.js API Client** is a fully type-safe SDK that is automatically generated from your backend's `AppRouter` definition. It's the bridge that connects your frontend application to your backend API, providing an exceptional developer experience with end-to-end type safety. This means no more manual type definitions for your API responses, no more guesswork about what parameters an endpoint expects, and no more out-of-sync frontend and backend code. If your backend API changes, TypeScript will immediately notify you of any errors in your frontend code that consumes it. ## 1. Creating the API Client The client is typically defined once in `src/igniter.client.ts`. It uses the `createIgniterClient` factory function from `@igniter-js/core/client`. ```typescript // src/igniter.client.ts import { createIgniterClient, useIgniterQueryClient } from '@igniter-js/core/client'; // This is a TYPE-ONLY import. No server code is bundled. import type { AppRouter } from './igniter.router'; /** * Type-safe API client generated from your Igniter router. * This is the main object you will use to interact with your API. */ export const api = createIgniterClient({ baseURL: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000', basePath: process.env.NEXT_PUBLIC_APP_BASE_PATH || '/api/v1', /** * This function dynamically provides the router definition. * - On the server (e.g., in Server Components), it returns the full router instance. * - On the client (browser), it returns only the router's schema, * which contains the API structure without any server-side logic. */ router: () => { if (typeof window === 'undefined') { // Server-side: Use the full router for direct function calls. return require('./igniter.router').AppRouter; } // Client-side: Use the lightweight schema for fetching. return require('./igniter.schema').AppRouterSchema; }, }); /** * A utility type to infer the type of the API client. * Useful for passing the client as props. */ export type ApiClient = typeof api; /** * A type-safe hook to get the query client instance. * Used for advanced cache manipulation, like manual invalidation. */ export const useQueryClient = useIgniterQueryClient(); ``` --- ## 2. Understanding the Client's Anatomy ### `createIgniterClient()` This is the factory function that builds your client. The crucial part is passing your `AppRouter` type as a generic argument: `createIgniterClient()`. This is what gives the client its "knowledge" of your API's structure, including all controllers, actions, input schemas, and output types. ### The `import type` Statement ```typescript import type { AppRouter } from './igniter.router'; ``` This is one of the most important lines. By using `import type`, we are telling TypeScript to only import the *type definition* of `AppRouter`, not the actual implementation code. This ensures that none of your backend server code (database connections, private logic, etc.) is ever accidentally bundled and sent to the client's browser. ### The Dynamic `router` Function The `router` property in the configuration is designed for **universal applications** (like Next.js) where code can run on both the server and the client. - `if (typeof window === 'undefined')`: This checks if the code is running in a Node.js environment (the server). If so, it `require`s the full `igniter.router`, allowing for direct, high-performance function calls without an HTTP round-trip. This is perfect for React Server Components (RSC) or server-side rendering (SSR). - `else`: If the code is running in a browser (`window` exists), it `require`s the `igniter.schema`. This is a lightweight JSON object containing only the API structure, which is used by the client to make actual HTTP requests. --- ## 3. How to Use the `api` Client The exported `api` object is your gateway to all backend operations. It mirrors the structure of your `AppRouter`. ### In Client Components (React) In client-side components, you use the React hooks attached to each action. ```tsx 'use client'; import { api } from '@/igniter.client'; function UserProfile({ userId }: { userId: string }) { // Access the query via `api.controllerKey.actionKey.useQuery()` const userQuery = api.users.getById.useQuery({ params: { id: userId }, // Type-safe parameters }); if (userQuery.isLoading) return

Loading...

; // `userQuery.data` is fully typed based on your backend action's return type. return

{userQuery.data?.user.name}

; } ``` ### In Server Components or Server Actions On the server, you can call the action's `.query()` or `.mutate()` method directly. This bypasses the HTTP layer for maximum performance. ```tsx // app/users/[id]/page.tsx (React Server Component) import { api } from '@/igniter.client'; export default async function UserPage({ params }: { params: { id: string } }) { // Call the action directly. No hook is needed. // The call is type-safe. const response = await api.users.getById.query({ params: { id: params.id }, }); // The `response` object is also fully typed. const user = response.user; return (

{user.name}

{user.email}

); } ``` This unified API client allows you to write data-fetching logic in a consistent way, whether you are on the server or the client, all with the guarantee of end-to-end type safety. --- ## Next Steps Now that you have a type-safe client, the next step is to provide it to your React application so the hooks can work: - [IgniterProvider](/docs/client-side/igniter-provider) - Learn how to set up the provider for React hooks - [useQuery](/docs/client-side/use-query) - Discover how to fetch data with type safety - [useMutation](/docs/client-side/use-mutation) - Learn how to perform mutations safely --- ### Full-Stack Guide: Building a High-Performance SPA with Bun, React, and Igniter.js **Category**: starter-guides **URL**: /docs/starter-guides/bun-react-starter-guide # Full-Stack Guide: Building a High-Performance SPA with Bun, React, and Igniter.js Welcome to the comprehensive guide for the Igniter.js Bun + React Starter. This document will take you on a journey to build an ultra-fast, modern, and fully type-safe Single Page Application (SPA). We'll harness the incredible speed of Bun as our server, runtime, and bundler, and pair it with the robust, type-safe API capabilities of Igniter.js. This starter is for developers who want to build a classic client-rendered React SPA but with a next-generation toolchain that offers unparalleled performance and a simplified, all-in-one developer experience. --- ## 1. Core Philosophy: Speed, Simplicity, and Safety This starter is built on three core principles, each enabled by its key technologies. ### 1.1. Speed and Simplicity with Bun Bun is the star of the show in this starter. It's a new, incredibly fast JavaScript runtime designed from the ground up for performance. In this project, Bun serves multiple roles, simplifying the toolchain significantly: - **Runtime**: It executes your server-side TypeScript code. - **Server**: We use Bun's native, high-performance `Bun.serve` API to handle HTTP requests. - **Bundler**: Bun's built-in bundler is used to package our React frontend for the browser. - **Package Manager**: Bun can be used as a drop-in replacement for `npm`, offering much faster dependency installation. This all-in-one approach reduces configuration overhead and provides a cohesive, lightning-fast development experience. ### 1.2. A Robust SPA Architecture This starter implements a classic, robust Single Page Application architecture. - The **Bun server** has two jobs: serve the static `index.html` file (the shell for our React app) for any non-API routes, and handle all API requests under the `/api/v1/*` path. - The **React frontend** is a pure client-side application. Once loaded, it takes over routing and rendering in the browser, communicating with the backend via type-safe API calls. - **Igniter.js** provides the entire backend API layer, bringing structure, scalability, and its signature end-to-end type safety to the project. ### 1.3. End-to-End Type Safety Just like in other Igniter.js starters, this is a non-negotiable feature. Igniter.js generates a type-safe client based on your API controllers. Your React application imports this client, giving you full IntelliSense and compile-time guarantees that your frontend and backend are always in sync. --- ## 2. Getting Started: From Zero to Running App Let's get the project installed and take a tour. ### Prerequisites - Bun (v1.0 or higher) - Docker and Docker Compose (for the database and Redis) ### Installation and Setup 1. **Initialize the Project**: Use the Igniter.js CLI to scaffold a new project. ```bash npx @igniter-js/cli init my-bun-app ``` When prompted, select **Bun + React** as your framework. Make sure to enable the **Store (Redis)** and **Queues (BullMQ)** features to get the full experience. 2. **Configure Environment**: `cd my-bun-app`. Rename `.env.example` to `.env`. The default URLs should work correctly with the Docker setup. 3. **Start Services**: Launch the PostgreSQL database and Redis instance. ```bash docker-compose up -d ``` 4. **Install & Sync DB**: Use Bun to install dependencies (it's much faster!) and then apply the Prisma schema. ```bash bun install bunx prisma db push ``` 5. **Run the Dev Server**: ```bash bun run dev ``` This command starts the `igniter dev` process, which in turn runs the Bun server with file-watching and hot-reloading enabled for both the backend and the React frontend. ### Project Structure Deep Dive The project structure is clean and organized for a full-stack SPA. ``` my-bun-app/ ├── public/ │ └── index.html # << The HTML shell for the React SPA ├── src/ │ ├── app/ # React page components │ ├── components/ # Shared React components │ ├── features/ # << Your application's business logic │ ├── services/ # Service initializations │ ├── index.tsx # << Unified Server & Client Entry Point │ ├── igniter.ts # Core Igniter.js initialization │ ├── igniter.client.ts # << Auto-generated Type-Safe Client │ └── igniter.router.ts # Main application router └── prisma/ └── schema.prisma ``` - **`src/index.tsx`**: This is the most unique file in this starter. It acts as the **unified entry point for both the server and the client**. - When run by Bun (on the server), it executes the `Bun.serve` block, which starts the HTTP server. This server inspects incoming request URLs. If the URL starts with `/api/v1/`, it passes the request to the Igniter.js router. Otherwise, it serves the `public/index.html` file. - When this file is processed by the bundler for the client, it ignores the server block and instead executes the React rendering logic (`ReactDOM.createRoot...`), mounting the main React component into the DOM. - **`public/index.html`**: The static HTML file that serves as the foundation for your React application. The bundled JavaScript will be injected into this file. - **`src/app/`**: Contains the top-level React components that act as "pages" in your SPA. - **`igniter.ts`, `igniter.router.ts`, `features/`**: These form the core of your backend, responsible for configuring Igniter.js, defining the API's shape, and housing all your business logic. - **`igniter.client.ts`**: The auto-generated, type-safe client that provides the React hooks (`.useQuery()`, `.useMutation()`) your SPA will use to communicate with the backend. --- ## 3. Building Our First Feature: A "Journal" API Let's build a simple daily journal application. ### Step 1: Define the Schema Open `prisma/schema.prisma` and add a `JournalEntry` model. ```prisma // prisma/schema.prisma model JournalEntry { id String @id @default(cuid()) content String mood String // e.g., "Happy", "Sad", "Productive" createdAt DateTime @default(now()) } ``` ### Step 2: Apply Database Changes Run `bunx prisma db push` to create the `JournalEntry` table. ```bash bunx prisma db push ``` ### Step 3: Scaffold the Feature with the CLI Use the `igniter generate` command to create the backend files automatically. ```bash bunx @igniter-js/cli generate feature journalEntries --schema prisma:JournalEntry ``` This command generates the controller, procedures, and Zod interfaces for your `JournalEntry` feature inside `src/features/journalEntries/`. ### Step 4: Register the Controller Open `src/igniter.router.ts` and register the new `journalEntriesController`. ```typescript // src/igniter.router.ts import { igniter } from '@/igniter'; import { exampleController } from '@/features/example'; // 1. Import the new controller import { journalEntriesController } from '@/features/journalEntries'; export const AppRouter = igniter.router({ controllers: { example: exampleController, // 2. Register it journalEntries: journalEntriesController, }, }); export type AppRouter = typeof AppRouter; ``` When you save this, the dev server will regenerate `igniter.client.ts`. The `api.journalEntries` client is now ready to be used by your React app. --- ## 4. Building the Frontend React SPA Now, let's build the UI for our journal. ### Displaying Journal Entries We'll create a component to fetch and display all entries. Create a new file at `src/features/journalEntries/presentation/components/JournalFeed.tsx`: ```tsx // src/features/journalEntries/presentation/components/JournalFeed.tsx import { api } from '@/igniter.client'; export function JournalFeed() { // Use the auto-generated hook to fetch data. const { data, isLoading, error } = api.journalEntries.list.useQuery(); if (isLoading) return

Loading journal...

; if (error) return

Error: {error.message}

; return (
{data?.journalEntries.map((entry) => (

{entry.content}

Mood: {entry.mood} | {new Date(entry.createdAt).toLocaleString()}
))}
); } ``` ### Creating an Entry Form Now for the form to add new entries. Create a new file at `src/features/journalEntries/presentation/components/CreateEntryForm.tsx`: ```tsx // src/features/journalEntries/presentation/components/CreateEntryForm.tsx import { api } from '@/igniter.client'; import { useState } from 'react'; export function CreateEntryForm() { const [content, setContent] = useState(''); const [mood, setMood] = useState('Productive'); const createEntryMutation = api.journalEntries.create.useMutation(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!content.trim()) return; createEntryMutation.mutate({ body: { content, mood } }, { onSuccess: () => { setContent(''); } }); }; return (