tagliatelle
v1.0.0-beta.8
Published
π The Declarative Backend Framework - JSX-powered API architecture on Fastify
Downloads
708
Maintainers
Readme
π <Tag>liatelle.js
The Declarative Backend Framework. Build APIs with JSX. Yes, really.
π Live Documentation | π¦ NPM Package
β οΈ Status: This is an experimental project. Most features are not tested or partially tested. Use at your own risk in production. Contributions and bug reports are welcome!
<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.
If you can write React, you can build a high-performance backend.
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 />);π€ 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.
π Quick Start
Create a new project
npx tagliatelle@beta 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@beta init creates a ready-to-run project |
π¦ Installation
New Project (Recommended)
npx tagliatelle@beta init my-apiAdd to Existing Project
npm install tagliatelle@betaThen 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@beta 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
π Disclaimer
This project started as a joke. And honestly? It still is.
But here's the thing β it actually works. You can build real APIs with it. The JSX compiles, the routes register, the middleware chains, and Fastify does its thing underneath.
Is it production-ready? Probably.
Is it a good idea? Debatable.
Is it fun? Absolutely.
Think of Tagliatelle.js as that friend who shows up to a formal dinner in a pasta costume β technically dressed, surprisingly functional, and definitely memorable.
Don't use it for:
- π NASA mission control systems
- π¦ Building Banks infra
- Semiconductors SOftware
Made with β€οΈ and plenty of carbs. Chahya Tayba ! πΉπ³
