tagliatelle
v1.0.1
Published
π The Declarative Backend Framework - JSX-powered API architecture on Fastify
Maintainers
Readme
π <Tag>liatelle.js
The Declarative Backend Framework. Build APIs with JSX. Yes, really.
π Live Documentation | π¦ NPM Package
<Tag>liatelle.js is a TypeScript backend framework built on top of Fastify that treats your API architecture like a component tree. Using JSX/TSX, you define your routes, middleware, and responses as a visual hierarchy.
import { render, Server, Logger, Cors, Routes } from 'tagliatelle';
import { Swagger } from './plugins/swagger.js';
const App = () => (
<Server port={3000}>
<Swagger title="My API" version="1.0.0" />
<Logger level="info" />
<Cors origin="*">
<Routes dir="./routes" />
</Cors>
</Server>
);
render(<App />);π Quick Start
Create a new project
npx tagliatelle init my-api
cd my-api
npm run devThat's it! Your API is running at http://localhost:3000 π
curl http://localhost:3000/health
# {"status":"Al Dente π","timestamp":"..."}
curl http://localhost:3000/posts
# {"success":true,"count":2,"data":[...]}π€ Why <Tag>liatelle.js?
| Feature | Description |
|---------|-------------|
| File-Based Routing | Next.js-style routing β your file structure IS your API |
| JSX Responses | Return <Response><Status code={201} /><Body data={...} /></Response> |
| JSX Middleware | Use <Err> and <Augment> for clean auth flows |
| JSX Config | Configure routes with <Logger>, <Middleware>, <RateLimiter> |
| Plugin System | Create custom tags with createPlugin β add Swagger, GraphQL, WebSockets, anything! |
| OpenAPI Schemas | Export GET_SCHEMA, POST_SCHEMA for auto-generated docs |
| Full TypeScript | End-to-end type safety with HandlerProps<TParams, TBody, TQuery> |
| Zero Boilerplate | Handlers return data or JSX β no res.send() needed |
| CLI Scaffolding | npx tagliatelle init creates a ready-to-run project |
π¦ Installation
New Project (Recommended)
npx tagliatelle init my-apiAdd to Existing Project
npm install tagliatelleThen configure your tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "tagliatelle"
}
}π Project Structure
my-api/
βββ server.tsx # Server entry point
βββ routes/ # File-based routing
β βββ _config.tsx # Global route config
β βββ index.tsx # GET /
β βββ health.tsx # GET /health
β βββ auth/ # Auth routes
β β βββ login.tsx # POST /auth/login
β β βββ register.tsx # POST /auth/register
β β βββ me.tsx # GET /auth/me
β βββ posts/
β βββ _config.tsx # Config for /posts/*
β βββ index.tsx # GET/POST /posts
β βββ [id].tsx # GET/PUT/DELETE /posts/:id
βββ plugins/ # Custom plugins
β βββ swagger.tsx # Swagger integration
βββ databases/ # Database providers
β βββ contentDB.ts # Content database
βββ middleware/
β βββ auth.tsx # JSX middleware
βββ tsconfig.json
βββ package.jsonExamples Folder (This Repo)
The examples/ folder contains a comprehensive demo showing all features:
examples/
βββ server.tsx # Multi-database server demo
βββ routes/ # Complete route examples
β βββ auth/ # Authentication routes
β βββ posts/ # Content CRUD
β βββ categories/ # Categories
β βββ tags/ # Tags
β βββ search/ # Search
β βββ pages/ # HTML pages (docs site source)
βββ plugins/ # Plugin examples
β βββ swagger.tsx # OpenAPI documentation
β βββ websocket.tsx # WebSocket support
β βββ graphql.tsx # GraphQL integration
β βββ metrics.tsx # Prometheus metrics
β βββ redis.tsx # Redis caching
βββ databases/ # Multi-database setup
β βββ authDB.ts # Auth database
β βββ contentDB.ts # Content database
βββ scripts/
β βββ build-docs.cjs # Build static docs for GitHub Pages
βββ docs/ # Generated static site
βββ index.html # Landing page
βββ docs.html # DocumentationRun the examples:
npm run example # Start server
npm run example:dev # Start with hot reloadπ½οΈ Server Configuration
server.tsx
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { render, Server, Logger, Cors, Routes } from 'tagliatelle';
import { Swagger } from './plugins/swagger.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const App = () => (
<Server port={3000}>
{/* Custom plugins! */}
<Swagger title="My API" version="1.0.0" path="/docs" />
<Logger level="info" />
<Cors origin="*">
<Routes dir={path.join(__dirname, 'routes')} />
</Cors>
</Server>
);
render(<App />);Server Components
| Component | Description |
|-----------|-------------|
| <Server port={3000}> | Main server wrapper |
| <Logger level="info" /> | Configure logging level |
| <Cors origin="*"> | Enable CORS |
| <Routes dir="./routes" /> | Load file-based routes |
| <RateLimiter max={100} timeWindow="1 minute" /> | Rate limiting |
| <Middleware use={fn} /> | Add global middleware |
Runtime Options
You can override server settings at runtime using CLI flags or environment variables:
| Option | Env Variable | Description |
|--------|--------------|-------------|
| -p, --port <number> | PORT | Port to listen on (default: 3000) |
| -H, --host <string> | HOST | Host to bind to (default: 0.0.0.0) |
| -o, --open | β | Open browser after server starts |
| -h, --help | β | Show help message |
Priority: CLI flags > Environment variables > Code defaults
π Plugin System (Custom Tags)
Create your own JSX components that hook into Fastify! Perfect for integrating third-party libraries.
Creating a Plugin
// plugins/swagger.tsx
import { createPlugin } from 'tagliatelle';
import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
interface SwaggerProps {
title?: string;
version?: string;
path?: string;
}
export const Swagger = createPlugin<SwaggerProps>(
'Swagger', // Plugin name (for logging)
async (fastify, props, config) => {
// You have full access to Fastify!
await fastify.register(swagger, {
openapi: {
info: {
title: props.title ?? 'API',
version: props.version ?? '1.0.0'
}
}
});
await fastify.register(swaggerUi, {
routePrefix: props.path ?? '/docs'
});
}
);Using Your Plugin
import { Swagger } from './plugins/swagger.js';
const App = () => (
<Server port={3000}>
<Swagger title="My API" version="1.0.0" path="/docs" />
<Routes dir="./routes" />
</Server>
);Plugin Examples
Here are plugins you can create:
GraphQL
import { createPlugin } from 'tagliatelle';
export const GraphQL = createPlugin<{ schema: GraphQLSchema }>(
'GraphQL',
async (fastify, props) => {
const mercurius = await import('mercurius');
await fastify.register(mercurius.default, {
schema: props.schema,
graphiql: true
});
}
);
// Usage: <GraphQL schema={mySchema} />WebSocket
import { createPlugin } from 'tagliatelle';
export const WebSocket = createPlugin<{ path?: string }>(
'WebSocket',
async (fastify, props) => {
const ws = await import('@fastify/websocket');
await fastify.register(ws.default);
fastify.get(props.path ?? '/ws', { websocket: true }, (socket) => {
socket.on('message', (msg) => socket.send(`Echo: ${msg}`));
});
}
);
// Usage: <WebSocket path="/ws" />Prometheus Metrics
import { createPlugin } from 'tagliatelle';
export const Metrics = createPlugin<{ path?: string }>(
'Metrics',
async (fastify, props) => {
const metrics = await import('fastify-metrics');
await fastify.register(metrics.default, {
endpoint: props.path ?? '/metrics'
});
}
);
// Usage: <Metrics path="/metrics" />Redis Cache
import { createPlugin } from 'tagliatelle';
export const Redis = createPlugin<{ url: string }>(
'Redis',
async (fastify, props) => {
const redis = await import('@fastify/redis');
await fastify.register(redis.default, { url: props.url });
}
);
// Usage: <Redis url="redis://localhost:6379" />Plugin API
createPlugin<TProps>(
name: string,
handler: (fastify, props, config) => Promise<void>
)| Parameter | Type | Description |
|-----------|------|-------------|
| name | string | Plugin name for logging |
| handler | PluginHandler | Async function that receives Fastify instance |
| fastify | FastifyInstance | Full access to register plugins, add routes, etc. |
| props | TProps | Props passed to the JSX component |
| config | RouteConfig | Current route configuration (middleware, prefix, etc.) |
π File-Based Routing
Your file structure becomes your API:
| File | Route |
|------|-------|
| routes/index.tsx | GET / |
| routes/health.tsx | GET /health |
| routes/posts/index.tsx | GET/POST /posts |
| routes/posts/[id].tsx | GET/PUT/DELETE /posts/:id |
| routes/users/[id]/posts.tsx | GET /users/:id/posts |
Route File Example
// routes/posts/[id].tsx
import { Response, Status, Body, Err } from 'tagliatelle';
import type { HandlerProps } from 'tagliatelle';
interface PostParams { id: string }
export async function GET({ params, log }: HandlerProps<PostParams>) {
log.info(`Fetching post ${params.id}`);
const post = await db.posts.find(params.id);
if (!post) {
return <Err code={404} message="Post not found" />;
}
return (
<Response>
<Status code={200} />
<Body data={{ success: true, data: post }} />
</Response>
);
}
export async function DELETE({ params }: HandlerProps<PostParams>) {
await db.posts.delete(params.id);
return (
<Response>
<Status code={200} />
<Body data={{ success: true, message: "Deleted" }} />
</Response>
);
}π OpenAPI Schema Support
Export schemas alongside your handlers for automatic OpenAPI documentation:
// routes/posts/index.tsx
import { Response, Status, Body, Err } from 'tagliatelle';
import type { HandlerProps } from 'tagliatelle';
// β¨ Export schemas for OpenAPI/Swagger
export const GET_SCHEMA = {
summary: 'List all posts',
description: 'Returns a paginated list of blog posts',
tags: ['posts'],
response: {
200: {
type: 'object',
properties: {
success: { type: 'boolean' },
count: { type: 'number' },
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
content: { type: 'string' }
}
}
}
}
}
}
};
export const POST_SCHEMA = {
summary: 'Create a new post',
tags: ['posts'],
body: {
type: 'object',
required: ['title', 'content'],
properties: {
title: { type: 'string', description: 'Post title' },
content: { type: 'string', description: 'Post content' }
}
},
response: {
201: {
type: 'object',
properties: {
success: { type: 'boolean' },
data: { type: 'object' }
}
}
}
};
// Your handlers
export async function GET({ log }: HandlerProps) {
const posts = await db.posts.findAll();
return (
<Response>
<Status code={200} />
<Body data={{ success: true, count: posts.length, data: posts }} />
</Response>
);
}
export async function POST({ body }: HandlerProps<unknown, CreatePostBody>) {
const post = await db.posts.create(body);
return (
<Response>
<Status code={201} />
<Body data={{ success: true, data: post }} />
</Response>
);
}Schema Naming Convention
| Export Name | HTTP Method |
|-------------|-------------|
| GET_SCHEMA | GET |
| POST_SCHEMA | POST |
| PUT_SCHEMA | PUT |
| DELETE_SCHEMA | DELETE |
| PATCH_SCHEMA | PATCH |
These schemas are automatically picked up by Swagger and other OpenAPI tools!
ποΈ Route Configuration
Create _config.tsx files to configure routes per directory:
routes/_config.tsx (Global)
import { Logger } from 'tagliatelle';
export default () => (
<>
<Logger level="info" />
</>
);routes/posts/_config.tsx (Posts-specific)
import { Logger, Middleware, RateLimiter } from 'tagliatelle';
import type { HandlerProps } from 'tagliatelle';
import { authMiddleware } from '../middleware/auth.js';
// Only require auth for write operations
const writeAuthMiddleware = async (props: HandlerProps, request, reply) => {
if (['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {
return; // Skip auth for reads
}
return authMiddleware(props, request, reply);
};
export default () => (
<>
<Logger level="debug" />
<RateLimiter max={100} timeWindow="1 minute" />
<Middleware use={writeAuthMiddleware} />
</>
);Config Inheritance
Configs are inherited and merged:
- Child configs override parent settings
- Middleware is additive (stacks)
- Nested directories inherit from parents
π€ JSX Responses
Return beautiful, declarative responses:
// Success response
return (
<Response>
<Status code={201} />
<Body data={{
success: true,
message: "Created!",
data: newItem
}} />
</Response>
);
// Error response (shorthand)
return <Err code={404} message="Not found" />;
// With custom headers
return (
<Response>
<Status code={200} />
<Headers headers={{ 'X-Custom': 'value' }} />
<Body data={result} />
</Response>
);Response Components
| Component | Description |
|-----------|-------------|
| <Response> | Wrapper for composing responses |
| <Status code={201} /> | Set HTTP status code |
| <Body data={{...}} /> | Set JSON response body |
| <Headers headers={{...}} /> | Set custom headers |
| <Err code={404} message="..." /> | Error response shorthand |
πΆοΈ Middleware
Middleware can use JSX components for responses and prop augmentation!
Creating Middleware
// middleware/auth.tsx
import { Augment, Err, authFailureTracker, isSafeString } from 'tagliatelle';
import type { HandlerProps, MiddlewareFunction } from 'tagliatelle';
export const authMiddleware: MiddlewareFunction = async (props, request, reply) => {
const apiKey = request.headers['x-api-key'];
// Return JSX error response
if (!apiKey || typeof apiKey !== 'string') {
return <Err code={401} message="Authentication required" />;
}
const user = await verifyToken(apiKey);
if (!user) {
return <Err code={401} message="Invalid credentials" />;
}
// Augment props with user data
return <Augment user={user} />;
};Middleware Factory Pattern
// Role-based authorization factory
export function requireRole(role: string): MiddlewareFunction {
return async (props, request, reply) => {
const user = props.user;
if (!user || user.role !== role) {
return <Err code={403} message="Access denied" />;
}
return; // Continue to handler
};
}
// Usage in _config.tsx
<Middleware use={requireRole('admin')} />Middleware Components
| Component | Description |
|-----------|-------------|
| <Err code={401} message="..." /> | Return error and halt chain |
| <Augment user={...} /> | Add data to handler props |
βοΈ Handler Props
Every handler receives typed props:
interface HandlerProps<TParams, TBody, TQuery> {
params: TParams; // URL parameters
query: TQuery; // Query string
body: TBody; // Request body
headers: Record<string, string>; // Request headers
request: FastifyRequest; // Raw Fastify request
reply: FastifyReply; // Raw Fastify reply
log: Logger; // Fastify logger
user?: unknown; // From auth middleware
db?: unknown; // From DB provider
}Example with Types
interface CreatePostBody {
title: string;
content: string;
}
export async function POST({ body, user, log }: HandlerProps<unknown, CreatePostBody>) {
log.info('Creating post');
if (!body.title) {
return <Err code={400} message="Title required" />;
}
const post = await createPost({ ...body, author: user.id });
return (
<Response>
<Status code={201} />
<Body data={{ success: true, data: post }} />
</Response>
);
}π Naming Conventions
| Pattern | Description |
|---------|-------------|
| index.tsx | Root of directory (/posts/index.tsx β /posts) |
| [param].tsx | Dynamic parameter (/posts/[id].tsx β /posts/:id) |
| [...slug].tsx | Catch-all (/docs/[...slug].tsx β /docs/*) |
| _config.tsx | Directory configuration (not a route) |
| _*.ts | Private files (ignored by router) |
π‘οΈ Security Utilities
Tagliatelle includes security helpers:
import {
authFailureTracker, // Rate limit auth failures by IP
isSafeString, // Validate string safety
sanitizeErrorMessage, // Clean error messages
safeErrorResponse, // Safe error responses
withTimeout, // Add timeouts to async operations
} from 'tagliatelle';π Performance
Built on Fastify, you get:
- 100k+ requests/second throughput
- Low latency JSON serialization
- Schema validation support
- Automatic logging
π CLI Reference
# Create a new project
npx tagliatelle init my-api
# Development with hot reload
npm run dev
# Production mode
npm run start
# Show runtime options (port, host, etc.)
npm run dev -- --helpπ Full Example
Here's a complete example with plugins, schemas, and middleware:
// server.tsx
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { render, Server, Logger, Cors, RateLimiter, Routes } from 'tagliatelle';
import { Swagger } from './plugins/swagger.js';
import { Metrics } from './plugins/metrics.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const App = () => (
<Server port={3000}>
{/* Plugins */}
<Swagger
title="My API"
version="1.0.0"
description="A delicious API"
tags={[
{ name: 'posts', description: 'Blog post operations' },
{ name: 'users', description: 'User management' }
]}
/>
<Metrics path="/metrics" />
{/* Configuration */}
<Logger level="info" />
<Cors origin="*">
<RateLimiter max={1000} timeWindow="1 minute">
<Routes dir={path.join(__dirname, 'routes')} />
</RateLimiter>
</Cors>
</Server>
);
render(<App />);π€ Contributing
Got a new "ingredient"? Open a Pull Request! With the plugin system, you can now contribute:
- [ ] Official plugin packages (
@tagliatelle/swagger,@tagliatelle/graphql, etc.) - [ ] More example plugins
- [ ] Documentation improvements
- [ ] Type improvements
- [ ] Performance optimizations
π License
MIT
π€ The Origin Story
This project started as a joke.
I noticed that every frontend framework is racing to become more server-oriented. React added Server Components. Next.js gave us "use server". Remix is basically a backend framework wearing a React costume. The JavaScript ecosystem is slowly but surely... becoming PHP.
So I thought: "If frontend devs want to write server code so badly, why not go all the way?"
Instead of sneaking server code into your React components, let's do the opposite β write your entire backend in pure TSX. Routes? JSX. Middleware? JSX. Responses? You guessed it... JSX.
Tagliatelle.js: Because if we're going to make everything look like PHP anyway, we might as well make it delicious. π
Why "Tagliatelle"?
<Tag>β Because we write everything in JSX tags.<Server>,<Route>,<Response>... it's tags all the way down.- Tagliatelle β It's pasta. Because frontend developers clearly want to write spaghetti code in the backend. π
At least this spaghetti is type-safe and al dente.
π Note
This project works and you can build real APIs with it. The JSX compiles, the routes register, the middleware chains, and Fastify does its thing underneath. That said, use your judgment for critical systems.
Made with β€οΈ and plenty of carbs. Chahya Tayba ! πΉπ³
