📚 Tagliatelle.js Documentation
Complete reference for building backend APIs with JSX.
All components, patterns, and advanced techniques.
Overview
Tagliatelle.js uses JSX to define server configuration, middleware composition,
and route responses. Component nesting creates a tree that defines request flow.
🎯 Core Principles
- Declarative - Define what, not how. Components describe behavior.
- Composable - Nest components to build complex middleware stacks.
- Props Flow Down - Parent components pass data to children.
- Middleware Augments - Middleware can add props for handlers.
Component Tree Architecture
Every Tagliatelle app is a tree. The root is <Server>, branches are
middleware, and leaves are <Routes>. Requests flow down through the tree.
<Server> ← Root: binds to port
└─ <Cors> ← Branch: adds CORS headers
└─ <Logger> ← Branch: logs requests
└─ <RateLimiter> ← Branch: limits requests
└─ <DB> ← Branch: provides database
└─ <Middleware> ← Branch: custom logic
└─ <Routes> ← Leaf: handles requests
Key concept: Each component wraps its children. A request passes
through each layer in order. Responses bubble back up.
<Server>
The root component. Creates an HTTP server and binds to a port.
Must wrap all other components.
| Prop | Type | Default | Description |
port | number | 3000 | Port to listen on |
host | string | "localhost" | Host to bind to |
<Server port={3000} host="0.0.0.0">
{/* All other components go here */}
</Server>
<Routes>
Loads route handlers from a directory. File paths become URL paths.
Use brackets [param] for dynamic segments.
| Prop | Type | Description |
dir | string | Path to routes directory (required) |
prefix | string | URL prefix for all routes in dir |
File-Based Routing
Routes are files in a directory. The file path becomes the URL path.
Use brackets for dynamic parameters.
| File Path |
URL |
Example |
routes/index.tsx |
/ |
Homepage |
routes/posts/index.tsx |
/posts |
List posts |
routes/posts/[id].tsx |
/posts/:id |
Single post |
routes/users/[id]/posts.tsx |
/users/:id/posts |
User's posts |
<Cors>
Adds Cross-Origin Resource Sharing headers. Wraps children with CORS middleware.
| Prop | Type | Description |
origin | string | boolean | string[] | Allowed origins. true = all |
methods | string[] | Allowed HTTP methods |
credentials | boolean | Allow credentials |
<Cors
origin={["https://app.com", "https://admin.com"]}
methods={["GET", "POST", "PUT", "DELETE"]}
credentials={true}
>
{children}
</Cors>
<Logger>
Structured logging for requests. Injects log into handler props.
| Prop | Type | Description |
level | "debug" | "info" | "warn" | "error" | Minimum log level |
// In handler, log is available via props:
export async function GET({ log }) {
log.info('Request received');
log.debug({ userId: 123 }, 'Debug data');
log.warn('Something unusual');
log.error('Something failed');
}
<RateLimiter>
Limits requests per time window. Returns 429 when exceeded.
| Prop | Type | Description |
max | number | Maximum requests per window |
timeWindow | string | Window duration (e.g., "1 minute") |
<RateLimiter max={100} timeWindow="1 minute">
{/* Max 100 requests per minute per IP */}
{children}
</RateLimiter>
<DB>
Provides database access. Injects db into handler props.
Multiple <DB> components can provide different databases to different routes.
| Prop | Type | Description |
provider | () => Database | Factory function that creates database instance |
name | string | Optional name for multiple DBs (access via props.dbs.name) |
// Database factory
export function createDB() {
return {
users: { findById: (id) => {...}, create: (data) => {...} },
posts: { findAll: () => [...], findById: (id) => {...} }
};
}
// In server.tsx
<DB provider={createDB}>
<Routes dir="./routes" />
</DB>
// In handler
export async function GET({ db }) {
const posts = db.posts.findAll();
}
<Middleware>
Wraps children with custom middleware function. Most flexible component.
Middleware can: augment props, halt requests, or pass through.
| Prop | Type | Description |
use | MiddlewareFunction | Middleware function to apply |
⚡ Middleware Return Values
- Return object → Props are merged with existing props for children
- Return JSX Response → Request is halted, response is sent
- Return undefined → Pass through to children unchanged
// Augmenting props
const timingMiddleware = (props, request) => {
return { startTime: Date.now() }; // Added to props
};
// Halting request
const authMiddleware = (props, request) => {
if (!request.headers.authorization) {
return (
<Response>
<Status code={401} />
<Body data={{ error: 'Unauthorized' }} />
</Response>
);
}
return { user: validateToken(request.headers.authorization) };
};
// Using in tree
<Middleware use={timingMiddleware}>
<Middleware use={authMiddleware}>
<Routes dir="./routes" />
</Middleware>
</Middleware>
<Group>
Groups multiple sibling components without adding behavior.
Useful for applying middleware to multiple Routes.
<Middleware use={authMiddleware}>
<Group>
<Routes dir="./routes/posts" prefix="/posts" />
<Routes dir="./routes/comments" prefix="/comments" />
<Routes dir="./routes/likes" prefix="/likes" />
</Group>
</Middleware>
<Response>
Container for HTTP response. Wraps Status, Headers, and Body components.
Return from route handlers.
return (
<Response>
<Status code={200} /> {/* Required: HTTP status */}
<Headers headers={{...}} /> {/* Optional: custom headers */}
<Body data={{...}} /> {/* Required: response body */}
</Response>
);
<Status>
Sets HTTP status code for the response.
| Prop | Type | Description |
code | number | HTTP status code (200, 201, 400, 404, 500, etc.) |
Sets custom HTTP headers on the response.
| Prop | Type | Description |
headers | Record<string, string> | Key-value pairs of header names and values |
<Headers headers={{
'Content-Type': 'application/json',
'Cache-Control': 'max-age=3600',
'X-Request-Id': requestId,
'Location': \`/posts/\${newPost.id}\`
}} />
<Body>
Sets the response body. Can be JSON, HTML string, or raw data.
| Prop | Type | Description |
data | any | Response body (objects auto-serialized to JSON) |
// JSON response (default)
<Body data={{ success: true, data: posts }} />
// HTML response
<Headers headers={{ 'Content-Type': 'text/html' }} />
<Body data={'<h1>Hello World</h1>'} />
// Array response
<Body data={[post1, post2, post3]} />
Props Chaining
Props flow from parent to child. Each middleware can augment props.
Handlers receive the accumulated props from all ancestors.
// Middleware 1: adds requestId
const requestIdMiddleware = () => ({
requestId: crypto.randomUUID()
});
// Middleware 2: adds timing
const timingMiddleware = () => ({
startTime: Date.now()
});
// Middleware 3: adds user (uses existing props)
const authMiddleware = (props, request) => {
props.log.info(\`Request \${props.requestId}\`);
return { user: getUser(request) };
};
// Tree: props accumulate
<Middleware use={requestIdMiddleware}> {/* props: { requestId } */}
<Middleware use={timingMiddleware}> {/* props: { requestId, startTime } */}
<Middleware use={authMiddleware}> {/* props: { requestId, startTime, user } */}
<Routes />
</Middleware>
</Middleware>
</Middleware>
// Handler receives all accumulated props
export async function GET({ requestId, startTime, user, db, log }) {
log.info(\`User \${user.id} on request \${requestId}\`);
}
Tree Nesting Patterns
Components can be nested in various patterns. The tree structure determines
which middleware applies to which routes.
Pattern 1: Linear Chain
All middleware applies to all routes in sequence.
<Cors>
<Logger>
<RateLimiter>
<DB>
<Routes /> {/* All middleware applies */}
</DB>
</RateLimiter>
</Logger>
</Cors>
Pattern 2: Sibling Branches
Different routes get different middleware.
<Logger>
{/* Public routes - no auth */}
<Routes dir="./routes/public" />
{/* Protected routes - with auth */}
<Middleware use={authMiddleware}>
<Routes dir="./routes/protected" />
</Middleware>
{/* Admin routes - with admin check */}
<Middleware use={adminMiddleware}>
<Routes dir="./routes/admin" />
</Middleware>
</Logger>
Pattern 3: Multiple Databases
Different DB components wrap different route groups.
<Logger>
{/* Auth routes use AuthDB */}
<DB provider={createAuthDB}>
<Routes dir="./routes/auth" prefix="/auth" />
</DB>
{/* Content routes use ContentDB */}
<DB provider={createContentDB}>
<Routes dir="./routes/posts" prefix="/posts" />
<Routes dir="./routes/categories" prefix="/categories" />
</DB>
</Logger>
Route Handlers
Export async functions named after HTTP methods: GET, POST,
PUT, PATCH, DELETE.
Handler Props (HandlerProps)
Every handler receives these props plus any added by middleware:
| Prop | Type | Description |
params | object | URL parameters from [brackets] |
query | object | Query string parameters (?key=value) |
body | any | Request body (parsed JSON) |
db | Database | Database from nearest <DB> ancestor |
log | Logger | Logger from nearest <Logger> ancestor |
request | FastifyRequest | Raw Fastify request object |
reply | FastifyReply | Raw Fastify reply object |
import { Response, Status, Body } from 'tagliatelle';
import type { HandlerProps } from 'tagliatelle';
interface PostParams {
id: string;
}
export async function GET({ params, db, log }: HandlerProps<PostParams>) {
const post = db.posts.findById(params.id);
if (!post) {
return (
<Response>
<Status code={404} />
<Body data={{ error: 'Post not found' }} />
</Response>
);
}
log.info({ postId: post.id }, '📄 Viewed post');
return (
<Response>
<Status code={200} />
<Body data={{ success: true, data: post }} />
</Response>
);
}
Config Files (_config.tsx)
Create _config.tsx in a route directory to apply middleware
to all routes in that directory and its subdirectories. The file exports
a component that receives children.
📁 Config File Rules
- File must be named
_config.tsx exactly
- Must export default a component function
- Component receives
{ children } prop
- Must render
{children} somewhere in the tree
- Applies to all routes in same directory and subdirectories
import { Middleware, Logger } from 'tagliatelle';
import { authMiddleware } from '../../middleware/auth';
// Applied to all routes in /posts/*
export default ({ children }) => (
<Logger level="debug">
<Middleware use={authMiddleware}>
{children} {/* Routes are rendered here */}
</Middleware>
</Logger>
);
Directory Structure with Config
routes/
├── posts/
│ ├── _config.tsx ← Applies to all below
│ ├── index.tsx ← GET /posts (has auth)
│ ├── [id].tsx ← GET /posts/:id (has auth)
│ └── comments/
│ ├── _config.tsx ← Additional middleware
│ └── index.tsx ← GET /posts/comments (has both configs)
└── public/
└── index.tsx ← No config, no auth
Authentication Patterns
Common patterns for handling authentication with Tagliatelle.
Pattern 1: Optional Auth Middleware
Middleware adds user to props if authenticated, doesn't block if not.
export const optionalAuth = async (props, request) => {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
return { isAuthenticated: false, user: null };
}
const user = validateToken(token);
return { isAuthenticated: true, user };
};
Pattern 2: Required Auth Middleware
Middleware blocks unauthenticated requests with 401.
export const requireAuth = async (props, request) => {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
return (
<Response>
<Status code={401} />
<Body data={{ error: 'Authentication required' }} />
</Response>
);
}
const user = validateToken(token);
if (!user) {
return (
<Response>
<Status code={401} />
<Body data={{ error: 'Invalid token' }} />
</Response>
);
}
return { user, isAuthenticated: true };
};
Pattern 3: Role-Based Access Control
Factory function that creates middleware for specific roles.
export const requireRole = (...roles: string[]) => {
return async (props, request) => {
// Assumes requireAuth ran first and added user to props
if (!props.user || !roles.includes(props.user.role)) {
return (
<Response>
<Status code={403} />
<Body data={{
error: 'Forbidden',
required: roles
}} />
</Response>
);
}
};
};
// Usage in tree
<Middleware use={requireAuth}>
<Middleware use={requireRole('admin', 'editor')}>
<Routes dir="./routes/admin" />
</Middleware>
</Middleware>
Custom Components & Wrappers
Tagliatelle supports creating your own reusable components and wrapper tags.
This is one of the most powerful features - compose your own abstractions!
Pattern 1: Simple Wrapper Component
Create a component that wraps children with specific middleware configuration.
Perfect for domain-specific route groups.
import { Middleware, RateLimiter, Logger } from 'tagliatelle';
import { apiKeyAuth } from '../middleware/auth';
// Custom wrapper for API routes with stricter rate limiting
export const ApiRoutes = ({ children }) => (
<RateLimiter max={60} timeWindow="1 minute">
<Logger level="debug">
<Middleware use={apiKeyAuth}>
{children}
</Middleware>
</Logger>
</RateLimiter>
);
// Usage in server.tsx:
<ApiRoutes>
<Routes dir="./routes/api/v1" prefix="/api/v1" />
<Routes dir="./routes/api/v2" prefix="/api/v2" />
</ApiRoutes>
Pattern 2: Configurable Wrapper
Accept props to make your wrapper configurable for different use cases.
import { Middleware, Logger } from 'tagliatelle';
import { requireAuth, requireRole } from '../middleware/auth';
interface ProtectedProps {
children: React.ReactNode;
roles?: ('admin' | 'editor' | 'user')[];
logLevel?: 'debug' | 'info';
}
export const ProtectedRoutes = ({
children,
roles,
logLevel = 'info'
}: ProtectedProps) => (
<Logger level={logLevel}>
<Middleware use={requireAuth}>
{roles ? (
<Middleware use={requireRole(...roles)}>
{children}
</Middleware>
) : (
children
)}
</Middleware>
</Logger>
);
// Usage:
<ProtectedRoutes roles={['admin']} logLevel="debug">
<Routes dir="./routes/admin" />
</ProtectedRoutes>
Pattern 3: Middleware Factory Functions
Create factory functions that return configured middleware.
This gives maximum flexibility for reusable logic.
import { Response, Status, Body } from 'tagliatelle';
// Factory: Create role checker for any roles
export const requireRole = (...allowedRoles: string[]) => {
return async (props, request) => {
if (!props.user || !allowedRoles.includes(props.user.role)) {
return (
<Response>
<Status code={403} />
<Body data={{
error: 'Forbidden',
required: allowedRoles
}} />
</Response>
);
}
};
};
// Factory: Add custom headers
export const addHeaders = (headers: Record<string, string>) => {
return async (props, request, reply) => {
Object.entries(headers).forEach(([key, value]) => {
reply.header(key, value);
});
};
};
// Factory: Validate request body
export const validateBody = (requiredFields: string[]) => {
return async (props, request) => {
const missing = requiredFields.filter(f => !request.body?.[f]);
if (missing.length > 0) {
return (
<Response>
<Status code={400} />
<Body data={{
error: 'Missing fields',
missing
}} />
</Response>
);
}
};
};
// Usage:
<Middleware use={requireRole('admin', 'moderator')}>
<Middleware use={addHeaders({ 'X-Api-Version': '2.0' })}>
<Routes dir="./routes/admin" />
</Middleware>
</Middleware>
Pattern 4: Domain-Specific Component
Create components for specific domains that bundle DB + routes + middleware.
import { Routes, DB, Middleware, RateLimiter } from 'tagliatelle';
import { createBlogDB } from '../databases/blogDB';
import { authMiddleware } from '../middleware/auth';
interface BlogModuleProps {
prefix?: string;
rateLimit?: number;
}
// Self-contained blog module with its own DB and routes
export const BlogModule = ({
prefix = '/blog',
rateLimit = 100
}: BlogModuleProps) => (
<RateLimiter max={rateLimit} timeWindow="1 minute">
<DB provider={createBlogDB}>
<Middleware use={authMiddleware}>
<Routes dir="./routes/blog/posts" prefix={\`\${prefix}/posts\`} />
<Routes dir="./routes/blog/comments" prefix={\`\${prefix}/comments\`} />
<Routes dir="./routes/blog/tags" prefix={\`\${prefix}/tags\`} />
</Middleware>
</DB>
</RateLimiter>
);
// Clean server.tsx:
<Server port={3000}>
<Logger>
<BlogModule prefix="/api/blog" rateLimit={50} />
<AuthModule prefix="/api/auth" />
<AdminModule prefix="/api/admin" />
</Logger>
</Server>
Pattern 5: Fastify Plugin Wrapper (Swagger Example)
Wrap Fastify plugins as Tagliatelle components. This example shows how to
integrate @fastify/swagger as a custom <Swagger> tag.
import type { TagliatelleNode } from 'tagliatelle';
// Swagger configuration props
export interface SwaggerProps {
children: TagliatelleNode;
title?: string;
description?: string;
version?: string;
routePrefix?: string;
}
/**
* Custom Swagger Component
* Wraps @fastify/swagger and @fastify/swagger-ui
*/
export const Swagger = ({
children,
title = 'My API',
description = 'API Documentation',
version = '1.0.0',
routePrefix = '/documentation',
}: SwaggerProps) => {
// In real implementation: register fastify plugins
// fastify.register(swagger, { openapi: { info: { title, description, version } } });
// fastify.register(swaggerUI, { routePrefix });
return children;
};
// Usage in server.tsx:
<Server port={3000}>
<Swagger
title="Tagliatelle API"
description="Built with JSX"
version="1.0.0"
routePrefix="/docs"
>
<Routes dir="./routes" />
</Swagger>
</Server>
This <Swagger> component demonstrates how to wrap Fastify plugins
as Tagliatelle tags. See components/Swagger.tsx for the implementation.
This pattern can be used for any Fastify plugin integration.
💡 Custom Component Rules
- Accept children - Always pass through
{children}
- Use built-in components - Compose from Middleware, Logger, DB, etc.
- Props for config - Make components configurable via props
- Return JSX - Component must return a valid component tree
- Factory for dynamic - Use factory functions for runtime-configured middleware
🔌 Plugin System (createPlugin)
The createPlugin API lets you wrap any Fastify plugin as a Tagliatelle JSX component.
Perfect for integrating Swagger, WebSocket, GraphQL, Redis, and more!
📦 Available Plugins
<Swagger> - OpenAPI documentation (@fastify/swagger)
<WS> - WebSocket support (@fastify/websocket)
<GraphQL> - GraphQL API (mercurius)
<Metrics> - Prometheus metrics (fastify-metrics)
<Redis> - Redis caching (@fastify/redis)
import { createPlugin } from 'tagliatelle';
import type { FastifyInstance } from 'fastify';
// Define your plugin props
interface MyPluginProps {
option1?: string;
option2?: number;
}
// Create the plugin with createPlugin
export const MyPlugin = createPlugin<MyPluginProps>(
'MyPlugin', // Plugin name for logs
async (fastify: FastifyInstance, props: MyPluginProps) => {
// Register Fastify plugins here
await fastify.register(somePlugin, {
option: props.option1 ?? 'default'
});
console.log('✓ MyPlugin loaded');
}
);
// Use as JSX in server.tsx:
<Server>
<MyPlugin option1="value" option2={42} />
<Routes dir="./routes" />
</Server>
Swagger Plugin Example
import { createPlugin } from 'tagliatelle';
export interface SwaggerProps {
title?: string;
version?: string;
path?: string;
tags?: Array<{ name: string; description?: string }>;
}
export const Swagger = createPlugin<SwaggerProps>(
'Swagger',
async (fastify, props) => {
try {
const swagger = await import('@fastify/swagger');
const swaggerUi = await import('@fastify/swagger-ui');
await fastify.register(swagger.default, {
openapi: {
info: {
title: props.title ?? 'API Documentation',
version: props.version ?? '1.0.0'
},
tags: props.tags
}
});
await fastify.register(swaggerUi.default, {
routePrefix: props.path ?? '/docs'
});
console.log(\`📚 Swagger → \${props.path ?? '/docs'}\`);
} catch {
console.log('⚠ Swagger skipped (install dependencies)');
}
}
);
// Usage:
<Swagger
title="My API"
version="2.0.0"
path="/swagger"
tags={[
{ name: 'users', description: 'User operations' },
{ name: 'posts', description: 'Post operations' }
]}
/>
WebSocket Plugin Example
export const WS = createPlugin<WSProps>(
'WebSocket',
async (fastify, props) => {
const websocket = await import('@fastify/websocket');
await fastify.register(websocket.default);
fastify.get(props.path ?? '/ws', { websocket: true }, (socket) => {
socket.on('message', (msg) => socket.send(\`Echo: \${msg}\`));
});
}
);
// Usage:
<WS path="/realtime" />
🔧 Plugin Handler Signature
type PluginHandler<TProps> = (
fastify: FastifyInstance, // The Fastify instance
props: TProps, // Props passed to the component
config: RouteConfig // Current route configuration
) => Promise<void> | void;
📖 Quick Reference - All Components
| Component | Purpose | Key Props |
<Server> | HTTP server root | port, host |
<Routes> | File-based routing | dir, prefix |
<Cors> | CORS headers | origin, methods |
<Logger> | Request logging | level |
<RateLimiter> | Rate limiting | max, timeWindow |
<DB> | Database provider | provider, name |
<Middleware> | Custom middleware | use |
<Group> | Group siblings | - |
<Response> | HTTP response | - |
<Status> | Status code | code |
<Headers> | Response headers | headers |
<Body> | Response body | data |