FastAPI for Node.js & Bun
β‘ 3.8x faster with Bun | π 89.5% of Fastify with Node.js
SyntroJS is production-ready with 937 passing tests and 71.55% code coverage. The core API is stable, though we're still adding features before v1.0.0.
- β Battle-tested - 937 tests across Node.js and Bun (99.3% passing)
- β Stable core API - We follow semantic versioning
- β Active development - Regular updates and community support
- π― v0.5.0 in progress - TOON Format + Architecture Refactor
Latest Release: v0.5.0 - TOON Format + Serialization Refactor - CHANGELOG
π‘ Note: While the core is stable, we recommend pinning to specific versions until v1.0.0
SyntroJS is the world's first dual-runtime framework that brings the simplicity and developer experience of FastAPI to the TypeScript ecosystem. Write your code once and run it on either Node.js for stability or Bun for maximum performance.
Coming in v0.5.0: TOON Format - reduce your API bandwidth costs 40-60% (like gRPC) while keeping responses human-readable and debuggable (like JSON). No compilation. No protobuf. Just savings. Perfect for any high-traffic API.
- π Dual Runtime Support: Write once, run on both Node.js and Bun. Zero code changes required.
- π₯ FastAPI-like Developer Experience: Automatic validation with Zod, full TypeScript type safety, elegant error handling (
HTTPException). - π¨ Automatic Interactive Docs: Beautiful landing page + Swagger UI + ReDoc out of the box at
/docs. - π§ͺ Testing Superpower:
SmartMutatorfor mutation testing in seconds. Type-safe client coming in v0.5.0. - π Rich Ecosystem: Middleware system, WebSockets, dependency injection, background tasks, structured logging.
- π Security First: JWT, OAuth2, API Keys, and security plugins built-in.
npm install syntrojs zodimport { SyntroJS } from 'syntrojs';
import { z } from 'zod';
const app = new SyntroJS({ title: 'My API' });
// Simple GET endpoint
app.get('/hello', { handler: () => ({ message: 'Hello World!' }) });
// POST with automatic validation
app.post('/users', {
body: z.object({
name: z.string().min(1),
email: z.string().email(),
}),
handler: ({ body }) => ({ id: 1, ...body }),
});
await app.listen(3000);That's it! π Visit http://localhost:3000/docs for interactive documentation.
// Permanent redirect (301 - SEO friendly)
app.get('/old-url', {
handler: ({ redirect }) => redirect('/new-url', 301)
});
// After form submission (303 - POST β GET)
app.post('/submit', {
body: z.object({ name: z.string() }),
handler: ({ body, redirect }) => {
saveData(body);
return redirect('/success', 303);
}
});// Serve JSON or HTML based on Accept header
app.get('/users', {
handler: ({ accepts }) => {
if (accepts.html()) {
return '<html><h1>Users</h1>...</html>';
}
// Default: JSON
return { users: [...] };
}
});
// TOON format ready (v0.5.0)
app.get('/data', {
handler: ({ accepts }) => {
if (accepts.toon()) {
return data; // Will be serialized as TOON
}
return data; // JSON
}
});Same code, different runtimes:
# Node.js (stability + full ecosystem)
node app.js
# Bun (maximum performance)
bun app.js| Runtime | Performance | Tests Passing | Use Case |
|---|---|---|---|
| Node.js | 89.5% of Fastify (5,819 req/s) | 728/728 (100%) | Production, plugins |
| Bun | 3.8x faster (~22,000 req/s) | 458/487 (94%) | Maximum speed |
| Feature | Node.js | Bun | Status |
|---|---|---|---|
| Core API | β Full | β Full | Identical |
| Plugins (CORS, Helmet, etc.) | β Full | v0.5.0 planned | |
| Static files | β Full | β Not available | v0.5.0 planned |
getRawFastify() |
β Works | β Use getRawServer() |
- |
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
import { app } from './app';
describe('API Tests', () => {
let server: string;
beforeAll(async () => {
server = await app.listen(0); // Random port
});
afterAll(async () => {
await app.close();
});
test('POST /users creates a user', async () => {
const port = new URL(server).port;
const res = await fetch(`http://localhost:${port}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John', email: 'john@example.com' })
});
const data = await res.json();
expect(res.status).toBe(200);
expect(data.name).toBe('John');
});
});Note: TinyTest is deprecated and will be removed in v0.5.0. See DEPRECATIONS.md for details.
Coming in v0.5.0: Type-safe client with autocomplete and zero code duplication!
| Method | Mutants | Tests | Time |
|---|---|---|---|
| Stryker (vanilla) | 1,247 | 187,050 | 43 min |
| SmartMutator | 142 | 284 | 12 sec |
pnpm test:mutateMutation Score: 58.72% (742 killed, 144 survived)
- Tests: 728/728 passing (Node.js 100%, Bun 94%)
- Coverage: 77.14% (Branch: 80.73%)
- Mutation Score: 58.72%
- Code Quality: 100% SOLID + DDD + Functional Programming
- Top Performers: RouteRegistry (100%), ZodAdapter (100%), DependencyInjector (95.83%)
const app = new SyntroJS({
title: 'Production API',
docs: false // β
REQUIRED for production
});- Disable all documentation (
docs: false) - Set proper CORS origins (not
*) - Enable rate limiting
- Configure structured logging without sensitive data
- Use environment variables for secrets
The Developer's Dilemma:
Traditionally, you had to choose between simplicity and efficiency:
| Feature | JSON | TOON π― | gRPC/Protobuf |
|---|---|---|---|
| Payload Size | 100% (large) | 40-50% β‘ | 35-45% |
| Human-Readable | β Yes | β Yes | β Binary |
| Debug with curl | β Easy | β Easy | β Requires tools |
| Setup Time | 5 minutes | 5 minutes | 2+ hours |
| Tooling Needed | None | None | protoc, plugins |
| Learning Curve | Low | Low | High |
| Type Safety | Runtime only | β Built-in | β Built-in |
| Production Costs | High | Low β‘ | Low |
TOON gives you the best of both worlds: gRPC's efficiency with JSON's simplicity.
High-Traffic APIs
Save thousands on cloud bills by reducing bandwidth 40-60%
Startups & MVPs
Get gRPC-level efficiency without the complexity overhead
Public APIs
Give developers human-readable responses that are also bandwidth-efficient
Mobile Apps
Reduce data usage for your users - faster loads, lower costs
Microservices
Efficient service-to-service communication without binary protocols
IoT Devices
Minimal bandwidth for resource-constrained environments
Bonus: LLM Integrations
Reduce token costs when sending API context to AI models
Same endpoint, different formats:
# JSON Response (traditional)
GET /api/users
Content-Length: 2,487 bytes
Monthly cost (1M requests): $225
# TOON Response (SyntroJS)
GET /api/users
Accept: application/toon
Content-Length: 1,024 bytes # -59%
Monthly cost (1M requests): $92 # Save $133/monthOne line of code:
const app = new SyntroJS({
serialization: 'toon' // β¨ That's it
});new SyntroJS(config?: SyntroJSConfig) - Creates a new SyntroJS application instance.
- Parameters:
config.title?: string- API title for OpenAPI docsconfig.version?: string- API versionconfig.description?: string- API descriptionconfig.logger?: boolean- Enable Fastify loggerconfig.syntroLogger?: LoggerIntegrationConfig | boolean- Enable @syntrojs/loggerconfig.runtime?: 'auto' | 'node' | 'bun'- Force specific runtimeconfig.docs?: boolean | object- Configure documentation endpointsconfig.fluentConfig?: object- Advanced adapter configuration
app.get(path, config) - Registers a GET route.
app.post(path, config) - Registers a POST route.
app.put(path, config) - Registers a PUT route.
app.delete(path, config) - Registers a DELETE route.
app.patch(path, config) - Registers a PATCH route.
- Parameters:
path: string- Route path (e.g.,/users/:id)config.handler: (ctx) => any- Route handler functionconfig.params?: ZodSchema- Path parameter validationconfig.query?: ZodSchema- Query parameter validationconfig.body?: ZodSchema- Request body validationconfig.response?: ZodSchema- Response validationconfig.status?: number- Default status codeconfig.dependencies?: object- Dependency injection
app.listen(port, host?) - Starts the HTTP server.
- Parameters:
port: number- Port to listen onhost?: string- Host address (default: '::')
- Returns:
Promise<string>- Server address
app.close() - Stops the HTTP server gracefully.
The request context is passed to all handlers:
interface RequestContext {
method: HttpMethod; // HTTP method
path: string; // Request path
params: any; // Path parameters
query: any; // Query parameters
body: any; // Request body
headers: Record<string, string>; // Request headers
cookies: Record<string, string>; // Cookies
correlationId: string; // Request tracking ID
timestamp: Date; // Request timestamp
dependencies: Record<string, any>; // Injected dependencies
background: {
addTask(task: () => void): void; // Queue background task
};
download(data, options): FileDownloadResponse; // File download helper
redirect(url, statusCode?): RedirectResponse; // Redirect helper
accepts: AcceptsHelper; // Content negotiation helper
}new ResponseHandler(serializerRegistry) - Creates response handler (used internally).
handler.serialize(result, statusCode, acceptHeader?) - Serializes response with content negotiation.
- Parameters:
result: any- Handler return valuestatusCode: number- HTTP status codeacceptHeader?: string- Accept header for content negotiation
- Returns:
Promise<SerializedResponseDTO> - Performance: O(1) for content-type based lookup
Implement IResponseSerializer interface:
interface IResponseSerializer {
canSerialize(result: any): boolean;
serialize(
result: any,
statusCode: number,
request: Request
): SerializedResponseDTO | null;
}
interface SerializedResponseDTO {
body: any; // Raw object or string
statusCode: number;
headers: Record<string, string>;
}Register custom serializer:
app.registerSerializer(new MySerializer(), 'MyFormat', ['application/my-format']);JsonSerializer - Default JSON serialization (raw objects).
TOONSerializer - Bandwidth-optimized format (40-60% reduction).
CustomResponseSerializer - Custom status/headers pattern.
RedirectSerializer - HTTP redirects (3xx).
StreamSerializer - Node.js Readable streams.
BufferSerializer - Binary buffers.
FileDownloadSerializer - File downloads with Content-Disposition.
inject(factory, options?) - Creates a dependency injection token.
- Parameters:
factory: (ctx?) => T- Factory functionoptions.scope?: 'singleton' | 'request'- Lifecycle scopeoptions.cleanup?: (instance) => Promise<void>- Cleanup function
- Returns: Dependency token
Example:
const dbService = inject(() => new Database(), { scope: 'singleton' });
app.get('/users', {
dependencies: { db: dbService },
handler: ({ dependencies }) => dependencies.db.getUsers()
});Helper function for pagination:
function paginate<T>(
items: T[],
page: number = 1,
limit: number = 10
) {
const offset = (page - 1) * limit;
const paginatedItems = items.slice(offset, offset + limit);
return {
data: paginatedItems,
pagination: {
page,
limit,
total: items.length,
totalPages: Math.ceil(items.length / limit),
hasNext: offset + limit < items.length,
hasPrev: page > 1
}
};
}Example:
app.get('/products', {
query: z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(10)
}),
handler: ({ query }) => {
const allProducts = getProducts();
return paginate(allProducts, query.page, query.limit);
}
});ctx.background.addTask(task, options?) - Queues asynchronous task (non-blocking).
- Parameters:
task: () => void | Promise<void>- Task functionoptions.name?: string- Task name (for logging)options.timeout?: number- Max execution time (ms)
Example:
app.post('/users', {
body: z.object({ email: z.string().email() }),
handler: ({ body, background }) => {
// Queue email send (non-blocking)
background.addTask(async () => {
await sendWelcomeEmail(body.email);
}, { name: 'welcome-email', timeout: 5000 });
return { success: true };
}
});Multipart form data is automatically parsed:
app.post('/upload', {
handler: ({ body, files }) => {
// files array contains uploaded files
const file = files[0];
return {
filename: file.filename,
size: file.size,
mimetype: file.mimetype
};
}
});File structure:
interface UploadedFile {
filename: string;
mimetype: string;
size: number;
buffer: Buffer;
stream: Readable;
}app.ws(path, handler) - Registers WebSocket endpoint.
app.ws('/chat', {
onConnect: (socket, request) => {
console.log('Client connected');
},
onMessage: (socket, message) => {
socket.send(`Echo: ${message}`);
},
onClose: (socket) => {
console.log('Client disconnected');
}
});Documentation is auto-generated from routes and schemas:
Endpoints (enabled by default):
GET /- Landing page with API overviewGET /docs- Swagger UIGET /redoc- ReDoc UIGET /openapi.json- OpenAPI 3.0 spec
Configure:
new SyntroJS({
title: 'My API',
version: '1.0.0',
description: 'API description',
docs: {
swagger: true, // Swagger UI
redoc: true, // ReDoc
landingPage: true,
openapi: true // JSON spec
}
});import { OAuth2PasswordBearer } from 'syntrojs';
const oauth2 = new OAuth2PasswordBearer({ tokenUrl: '/token' });
app.get('/protected', {
dependencies: { user: oauth2 },
handler: ({ dependencies }) => {
return { user: dependencies.user };
}
});import { HTTPBearer } from 'syntrojs';
const bearer = new HTTPBearer();
app.get('/api/data', {
dependencies: { token: bearer },
handler: ({ dependencies }) => {
const token = dependencies.token; // Validated token
return { data: 'protected' };
}
});import { APIKeyHeader, APIKeyQuery } from 'syntrojs';
// Via header
const apiKeyHeader = new APIKeyHeader({ name: 'X-API-Key' });
// Via query parameter
const apiKeyQuery = new APIKeyQuery({ name: 'api_key' });
app.get('/api/data', {
dependencies: { apiKey: apiKeyHeader },
handler: ({ dependencies }) => {
const key = dependencies.apiKey; // Validated key
return { data: 'protected' };
}
});import { HTTPBasic } from 'syntrojs';
const basicAuth = new HTTPBasic();
app.get('/admin', {
dependencies: { credentials: basicAuth },
handler: ({ dependencies }) => {
const { username, password } = dependencies.credentials;
// Validate credentials
return { admin: true };
}
});app.use(middleware) - Registers global middleware.
app.use(path, middleware) - Registers path-specific middleware.
interface Middleware {
(ctx: RequestContext, next: () => Promise<void>): Promise<void>;
}Example:
// Global middleware
app.use(async (ctx, next) => {
console.log(`${ctx.method} ${ctx.path}`);
await next();
});
// Path-specific
app.use('/api/*', async (ctx, next) => {
if (!ctx.headers.authorization) {
throw new UnauthorizedException('Token required');
}
await next();
});app.registerExceptionHandler(errorClass, handler) - Registers custom error handler.
app.registerExceptionHandler(MyError, (error, ctx) => ({
status: 400,
body: { error: error.message }
}));Built-in Exceptions:
BadRequestException(400)UnauthorizedException(401)ForbiddenException(403)NotFoundException(404)ConflictException(409)UnprocessableEntityException(422)ServiceUnavailableException(503)
app.getSerializerRegistry() - Access serializer registry.
app.getMiddlewareRegistry() - Access middleware registry.
app.getRouteRegistry() - Access route registry.
app.getWebSocketRegistry() - Access WebSocket registry.
- File downloads (
ctx.download()) - Streaming responses
- File uploads (multipart)
- Form data support
- HTTP redirects (
ctx.redirect()) - Content negotiation (
ctx.accepts)
TOON: The sweet spot between JSON's simplicity and gRPC's efficiency
- TOON Format Support: 40-60% payload reduction for any API
- β
Human-readable (like JSON) - debug with
curl - β No compilation needed (like JSON) - no protoc, no tooling
- β Efficient (like gRPC) - 40-60% smaller payloads
- β
Official
@toon-format/toonpackage integration - β Content negotiation via Accept header
- β Perfect for: High-traffic APIs, mobile apps, microservices, public APIs
- β
Human-readable (like JSON) - debug with
- Serialization Architecture Refactor
- β ResponseHandler centralized (SOLID)
- β Adapters unified: 5 β 2 (FluentAdapter + BunAdapter)
- β DTO-based serialization (runtime-agnostic)
- β O(1) content negotiation
- β Bundle size: -21KB (-10%)
- Native Bun plugins (CORS, Helmet, etc.)
- Server-Sent Events (SSE)
- CSRF protection
Why TOON over gRPC?
- No protobuf compilation required
- Readable in browser DevTools & logs
- Simple setup (5 minutes vs 2 hours)
- Same cost savings, zero complexity
- Works with any HTTP client (curl, fetch, axios)
- Static file serving (optional)
- Template rendering integrations (optional)
- Additional middleware helpers (optional)
- Official CLI (
create-syntrojs) - Graceful shutdown
- Complete documentation (Docusaurus)
- Migration guides (Express, Fastify)
- Production deployment guide
- Security audit
- gRPC Support - For maximum performance scenarios
- Use alongside TOON for hybrid architectures
- Public APIs: TOON (DX), Internal: gRPC (performance)
- GraphQL integration
- ORM adapters (Prisma, TypeORM, Drizzle)
- Metrics/Prometheus integration
- Distributed tracing (OpenTelemetry)
- Examples Repository: syntrojs-example
- Architecture: ARCHITECTURE.md
- Full Documentation: Coming soon
We welcome contributions! Check out our GitHub repository.
Apache 2.0 - See LICENSE for details.