Response Types
Master all response types in Igniter.js including success, error, streaming, cookies, headers, and cache revalidation for building robust APIs.
Overview
Response Types in Igniter.js provide a comprehensive, type-safe API for constructing HTTP responses. The response object available in action handlers gives you full control over status codes, headers, cookies, body content, streaming, and client-side cache revalidation.
const userController = igniter.controller({
path: '/users',
actions: {
getUser: igniter.query({
path: '/:id' as const,
handler: async ({ request, response, context }) => {
const user = await context.db.users.findUnique({
where: { id: request.params.id }
});
if (!user) {
return response.notFound('User not found');
}
return response.success({ user });
}
})
}
});Fluent Interface
All response methods are chainable, allowing you to build complex responses in a fluent, type-safe manner.
Success Responses
response.success()
Returns a 200 OK response with data:
const action = igniter.query({
path: '/users',
handler: async ({ response, context }) => {
const users = await context.db.users.findMany();
// ✅ Returns 200 OK with typed data
return response.success({ users });
}
});TypeScript Inference:
const result = response.success({ users: [] });
// result is typed as: IgniterResponse<{ users: User[] }>response.created()
Returns a 201 Created response, typically after creating a resource:
const createUser = igniter.mutation({
path: '/users',
method: 'POST',
body: CreateUserSchema,
handler: async ({ request, response, context }) => {
const user = await context.db.users.create({
data: request.body
});
// ✅ Returns 201 Created
return response.created({ user });
}
});response.noContent()
Returns a 204 No Content response (commonly used for DELETE operations):
const deleteUser = igniter.mutation({
path: '/users/:id' as const,
method: 'DELETE',
handler: async ({ request, response, context }) => {
await context.db.users.delete({
where: { id: request.params.id }
});
// ✅ Returns 204 No Content (no body)
return response.noContent();
}
});RFC 7231 Compliance
response.noContent() returns HTTP 204 with no body and no Content-Type header, compliant with RFC 7231.
response.json()
Generic JSON response (you can control status manually):
const action = igniter.query({
handler: async ({ response }) => {
return response
.status(200)
.json({ message: 'Custom JSON response' });
}
});Error Responses
response.badRequest()
Returns 400 Bad Request:
const action = igniter.mutation({
handler: async ({ request, response }) => {
if (!request.body.email) {
return response.badRequest('Email is required');
}
// Continue...
}
});With Additional Data:
return response.badRequest('Validation failed', {
fields: {
email: 'Invalid format',
password: 'Too short'
}
});response.unauthorized()
Returns 401 Unauthorized:
const protectedAction = igniter.query({
handler: async ({ context, response }) => {
if (!context.user) {
return response.unauthorized('Authentication required');
}
// User is authenticated
}
});response.forbidden()
Returns 403 Forbidden (authenticated but not allowed):
const deletePost = igniter.mutation({
path: '/posts/:id' as const,
method: 'DELETE',
handler: async ({ request, context, response }) => {
const post = await context.db.posts.findUnique({
where: { id: request.params.id }
});
if (post.authorId !== context.user.id) {
return response.forbidden('You can only delete your own posts');
}
// User is authorized
await context.db.posts.delete({ where: { id: post.id } });
return response.noContent();
}
});response.notFound()
Returns 404 Not Found:
const getUser = igniter.query({
path: '/users/:id' as const,
handler: async ({ request, response, context }) => {
const user = await context.db.users.findUnique({
where: { id: request.params.id }
});
if (!user) {
return response.notFound('User not found');
}
return response.success({ user });
}
});response.error()
Generic error handler with custom error code:
import { IgniterResponseError } from '@igniter-js/core';
const action = igniter.mutation({
handler: async ({ response }) => {
const error = new IgniterResponseError({
code: 'ERR_CUSTOM',
message: 'Something went wrong',
data: { details: 'Additional error context' }
});
return response
.status(500)
.error(error);
}
});Redirects
response.redirect()
Creates a redirect response:
const legacyRoute = igniter.query({
path: '/old-route',
handler: async ({ response }) => {
// ✅ Returns 302 redirect (default)
return response.redirect('/new-route', 'replace');
}
});Redirect Types:
'replace': Replaces current URL (default, HTTP 302)'push': Adds to browser history (HTTP 302)
Custom Status:
return response
.status(301) // Permanent redirect
.redirect('/new-route', 'replace');Headers and Cookies
response.setHeader()
Set custom headers:
const action = igniter.query({
handler: async ({ response }) => {
return response
.setHeader('Cache-Control', 'public, max-age=3600')
.setHeader('X-Custom-Header', 'value')
.success({ data: [] });
}
});response.setCookie()
Set cookies with full control:
const login = igniter.mutation({
path: '/auth/login',
method: 'POST',
handler: async ({ response }) => {
const token = 'jwt-token-here';
return response
.setCookie('session', token, {
httpOnly: true, // ✅ Not accessible via JavaScript
secure: true, // ✅ HTTPS only
sameSite: 'strict', // ✅ CSRF protection
maxAge: 3600, // ✅ 1 hour
path: '/',
domain: 'example.com'
})
.success({ message: 'Logged in' });
}
});Cookie Options:
| Option | Type | Description |
|---|---|---|
httpOnly | boolean | Prevents JavaScript access (security) |
secure | boolean | HTTPS only |
sameSite | 'strict' | 'lax' | 'none' | CSRF protection |
maxAge | number | Max age in seconds |
expires | Date | Absolute expiration date |
path | string | Cookie path |
domain | string | Cookie domain |
partitioned | boolean | CHIPS (partitioned cookies) |
prefix | 'secure' | 'host' | Cookie prefix (__Secure- or __Host-) |
Secure Cookie Prefixes:
// __Secure- prefix (requires HTTPS)
response.setCookie('token', 'value', {
prefix: 'secure' // ✅ Auto-sets secure: true
});
// __Host- prefix (requires HTTPS, path=/, no domain)
response.setCookie('token', 'value', {
prefix: 'host' // ✅ Auto-sets secure, path, removes domain
});Cookie Prefix Enforcement
Igniter.js automatically enforces security requirements for __Secure- and __Host- cookie prefixes according to RFC 6265bis.
Status Codes
response.status()
Manually set HTTP status:
const action = igniter.mutation({
handler: async ({ response }) => {
return response
.status(418) // ✅ I'm a teapot
.json({ message: "I'm a teapot" });
}
});Common Status Codes:
| Code | Method | Description |
|---|---|---|
| 200 | success() | OK |
| 201 | created() | Created |
| 204 | noContent() | No Content |
| 400 | badRequest() | Bad Request |
| 401 | unauthorized() | Unauthorized |
| 403 | forbidden() | Forbidden |
| 404 | notFound() | Not Found |
| 302 | redirect() | Redirect |
Real-Time Streaming (SSE)
response.stream()
Create Server-Sent Events streams for real-time updates:
const liveNotifications = igniter.query({
path: '/notifications/live',
handler: async ({ response }) => {
return response.stream({
channelId: 'notifications:live',
// Optional: send data immediately on connection
initialData: {
status: 'connected',
timestamp: new Date().toISOString()
},
// Optional: filter messages
filter: (msg) => msg.data.priority === 'high',
// Optional: transform messages before sending
transform: (msg) => ({
...msg,
data: { ...msg.data, seen: false }
})
});
}
});Publishing Events:
import { SSEProcessor } from '@igniter-js/core';
// From anywhere in your app
SSEProcessor.publishEvent({
channel: 'notifications:live',
type: 'notification',
data: {
id: '123',
message: 'New order received',
priority: 'high'
}
});Client Connection:
// Client receives connection info
const result = await client.notifications.live.query();
// result.data contains:
// {
// type: 'stream',
// channelId: 'notifications:live',
// connectionInfo: {
// endpoint: '/api/v1/sse/events',
// params: { channels: 'notifications:live' }
// }
// }
// Connect to SSE endpoint
const eventSource = new EventSource(
`${result.data.connectionInfo.endpoint}?channels=${result.data.connectionInfo.params.channels}`
);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('New notification:', data);
};SSE Architecture
Igniter.js uses a centralized SSE endpoint (/api/v1/sse/events) for all real-time streams. Actions return connection information instead of creating individual SSE endpoints.
Cache Revalidation
response.revalidate()
Invalidate client-side cache for specific queries:
const createPost = igniter.mutation({
path: '/posts',
method: 'POST',
body: CreatePostSchema,
handler: async ({ request, response, context }) => {
const post = await context.db.posts.create({
data: request.body
});
// ✅ Invalidate 'posts' query on all clients
return response
.revalidate(['posts', 'dashboard'])
.created({ post });
}
});With Scoped Revalidation:
const updatePost = igniter.mutation({
path: '/posts/:id' as const,
method: 'PATCH',
handler: async ({ request, response, context }) => {
const post = await context.db.posts.update({
where: { id: request.params.id },
data: request.body
});
// ✅ Revalidate only for specific tenant
return response
.revalidate(['posts', 'user-posts'], async (ctx) => {
return [`tenant:${ctx.tenantId}`];
})
.success({ post });
}
});Revalidation Options:
| Option | Type | Description |
|---|---|---|
queryKeys | string | string[] | Query keys to invalidate |
data | any | Optional data to send with revalidation |
broadcast | boolean | Broadcast to all clients (default: true) |
scopes | ScopeResolver | Function returning scope IDs |
Real-Time Cache Updates
When you call response.revalidate(), Igniter.js automatically notifies all connected clients via SSE to refetch invalidated queries!
Advanced Response Patterns
Chaining Methods
All response methods are chainable:
const action = igniter.mutation({
handler: async ({ response }) => {
return response
.status(201)
.setHeader('X-Request-ID', 'abc123')
.setCookie('session', 'token', { httpOnly: true })
.revalidate(['users'])
.created({ user });
}
});Conditional Responses
const getPost = igniter.query({
path: '/posts/:id' as const,
handler: async ({ request, response, context }) => {
const post = await context.db.posts.findUnique({
where: { id: request.params.id }
});
if (!post) {
return response.notFound('Post not found');
}
if (post.published === false && post.authorId !== context.user?.id) {
return response.forbidden('This post is not published');
}
return response.success({ post });
}
});Multiple Cookies
const login = igniter.mutation({
handler: async ({ response }) => {
return response
.setCookie('accessToken', 'token1', { maxAge: 900 }) // 15 min
.setCookie('refreshToken', 'token2', { maxAge: 604800 }) // 7 days
.setCookie('userPrefs', 'theme=dark', { maxAge: 31536000 }) // 1 year
.success({ message: 'Logged in' });
}
});Error Response Structure
All error responses follow this structure:
{
"data": null,
"error": {
"code": "ERR_NOT_FOUND",
"message": "User not found",
"data": {
"userId": "123"
}
}
}Success Response Structure:
{
"data": {
"user": {
"id": "123",
"name": "John"
}
},
"error": null
}Discriminated Union
The IgniterResponse type is a discriminated union: either data is present (and error is null), or error is present (and data is null). Never both!
Best Practices
1. Use Semantic Response Methods
// ✅ Good - Clear intent
return response.created({ user });
// ❌ Bad - Less clear
return response.status(201).json({ user });2. Set Cookies Securely
// ✅ Good - Secure by default
return response.setCookie('session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
});
// ❌ Bad - Insecure
return response.setCookie('session', token);3. Provide Meaningful Error Messages
// ✅ Good - Helpful message
return response.notFound(`User with ID "${request.params.id}" not found`);
// ❌ Bad - Generic message
return response.notFound();4. Use Revalidation for Mutations
// ✅ Good - Auto-update clients
return response
.revalidate(['posts'])
.created({ post });
// ❌ Bad - Clients don't know data changed
return response.created({ post });Common Patterns
CRUD Operations
const postController = igniter.controller({
path: '/posts',
actions: {
// List
list: igniter.query({
handler: async ({ response }) => {
const posts = await db.posts.findMany();
return response.success({ posts });
}
}),
// Get
get: igniter.query({
path: '/:id' as const,
handler: async ({ request, response }) => {
const post = await db.posts.findUnique({
where: { id: request.params.id }
});
if (!post) return response.notFound();
return response.success({ post });
}
}),
// Create
create: igniter.mutation({
method: 'POST',
body: CreatePostSchema,
handler: async ({ request, response }) => {
const post = await db.posts.create({ data: request.body });
return response.revalidate(['posts']).created({ post });
}
}),
// Update
update: igniter.mutation({
path: '/:id' as const,
method: 'PATCH',
body: UpdatePostSchema,
handler: async ({ request, response }) => {
const post = await db.posts.update({
where: { id: request.params.id },
data: request.body
});
return response.revalidate(['posts']).success({ post });
}
}),
// Delete
delete: igniter.mutation({
path: '/:id' as const,
method: 'DELETE',
handler: async ({ request, response }) => {
await db.posts.delete({ where: { id: request.params.id } });
return response.revalidate(['posts']).noContent();
}
})
}
});Authentication Flow
const authController = igniter.controller({
path: '/auth',
actions: {
login: igniter.mutation({
path: '/login',
method: 'POST',
body: LoginSchema,
handler: async ({ request, response }) => {
const { email, password } = request.body;
const user = await db.users.findUnique({ where: { email } });
if (!user) {
return response.unauthorized('Invalid credentials');
}
const valid = await verifyPassword(password, user.passwordHash);
if (!valid) {
return response.unauthorized('Invalid credentials');
}
const token = generateToken(user);
return response
.setCookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600
})
.success({ user: { id: user.id, name: user.name, email: user.email } });
}
}),
logout: igniter.mutation({
path: '/logout',
method: 'POST',
handler: async ({ response }) => {
return response
.setCookie('session', '', { maxAge: 0 })
.success({ message: 'Logged out' });
}
})
}
});Next Steps
Validation
Master input validation in Igniter.js using Zod, Valibot, Yup, or any StandardSchemaV1-compliant library for type-safe request handling.
Error Handling
Master error handling in Igniter.js with IgniterError, IgniterResponseError, validation errors, custom error codes, and global error handlers.