📚 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.

Tree Structure Visualization txt
<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.

PropTypeDefaultDescription
portnumber3000Port to listen on
hoststring"localhost"Host to bind to
Basic Server tsx
<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.

PropTypeDescription
dirstringPath to routes directory (required)
prefixstringURL 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.

PropTypeDescription
originstring | boolean | string[]Allowed origins. true = all
methodsstring[]Allowed HTTP methods
credentialsbooleanAllow credentials
CORS Configuration tsx
<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.

PropTypeDescription
level"debug" | "info" | "warn" | "error"Minimum log level
Using Logger in Handlers tsx
// 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.

PropTypeDescription
maxnumberMaximum requests per window
timeWindowstringWindow duration (e.g., "1 minute")
Rate Limiting tsx
<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.

PropTypeDescription
provider() => DatabaseFactory function that creates database instance
namestringOptional name for multiple DBs (access via props.dbs.name)
Database Provider tsx
// 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.

PropTypeDescription
useMiddlewareFunctionMiddleware 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
Custom Middleware tsx
// 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.

Grouping Routes tsx
<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.

Response Structure tsx
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.

PropTypeDescription
codenumberHTTP status code (200, 201, 400, 404, 500, etc.)

<Headers>

Sets custom HTTP headers on the response.

PropTypeDescription
headersRecord<string, string>Key-value pairs of header names and values
Common Headers tsx
<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.

PropTypeDescription
dataanyResponse body (objects auto-serialized to JSON)
Body Types tsx
// 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.

Props Flow Example tsx
// 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.

Linear Chain tsx
<Cors> <Logger> <RateLimiter> <DB> <Routes /> {/* All middleware applies */} </DB> </RateLimiter> </Logger> </Cors>

Pattern 2: Sibling Branches

Different routes get different middleware.

Sibling Branches tsx
<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.

Multi-DB Pattern tsx
<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:

PropTypeDescription
paramsobjectURL parameters from [brackets]
queryobjectQuery string parameters (?key=value)
bodyanyRequest body (parsed JSON)
dbDatabaseDatabase from nearest <DB> ancestor
logLoggerLogger from nearest <Logger> ancestor
requestFastifyRequestRaw Fastify request object
replyFastifyReplyRaw Fastify reply object
routes/posts/[id].tsx tsx
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
routes/posts/_config.tsx tsx
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

File Structure txt
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.

middleware/optionalAuth.tsx tsx
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.

middleware/requireAuth.tsx tsx
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.

middleware/requireRole.tsx tsx
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.

components/ApiRoutes.tsx tsx
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.

components/ProtectedRoutes.tsx tsx
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.

middleware/factories.tsx tsx
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.

components/BlogModule.tsx tsx
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.

components/Swagger.tsx tsx
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>
💡 Pattern Example
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)
Creating a Custom Plugin tsx
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

plugins/swagger.tsx tsx
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

plugins/websocket.tsx tsx
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

ComponentPurposeKey Props
<Server>HTTP server rootport, host
<Routes>File-based routingdir, prefix
<Cors>CORS headersorigin, methods
<Logger>Request logginglevel
<RateLimiter>Rate limitingmax, timeWindow
<DB>Database providerprovider, name
<Middleware>Custom middlewareuse
<Group>Group siblings-
<Response>HTTP response-
<Status>Status codecode
<Headers>Response headersheaders
<Body>Response bodydata