Package Exports
- @spfn/core
- @spfn/core/client
- @spfn/core/codegen
- @spfn/core/db
- @spfn/core/env
- @spfn/core/route
- @spfn/core/server
Readme
@spfn/core
Core framework for building type-safe backend APIs with Next.js and Hono
⚠️ Alpha Release: SPFN is currently in alpha. APIs may change. Use
@alphatag for installation.
Installation
Recommended: Create New Project
npx spfn@alpha create my-appAdd to Existing Next.js Project
cd your-nextjs-project
npx spfn@alpha initManual Installation
npm install @spfn/core hono drizzle-orm postgres @sinclair/typeboxQuick Start
1. Define a Contract
// src/server/routes/users/contract.ts
import { Type } from '@sinclair/typebox';
export const getUsersContract = {
method: 'GET' as const,
path: '/',
query: Type.Object({
page: Type.Optional(Type.Number()),
limit: Type.Optional(Type.Number()),
}),
response: Type.Object({
users: Type.Array(Type.Object({
id: Type.Number(),
name: Type.String(),
email: Type.String(),
})),
total: Type.Number(),
}),
};2. Create a Route
// src/server/routes/users/index.ts
import { createApp } from '@spfn/core/route';
import { getUsersContract } from './contract.js';
import { Repository } from '@spfn/core/db';
import { users } from '../../entities/users.js';
const app = createApp();
app.bind(getUsersContract, async (c) => {
const { page = 1, limit = 10 } = c.query;
// Create repository instance
const repo = new Repository(users);
const offset = (page - 1) * limit;
const result = await repo.select()
.limit(limit)
.offset(offset);
return c.json({ users: result, total: result.length });
});
export default app;3. Start Server
npm run spfn:dev
# Server starts on http://localhost:8790Architecture Pattern
SPFN follows a layered architecture that separates concerns and keeps code maintainable:
┌─────────────────────────────────────────┐
│ Routes Layer │ HTTP handlers, contracts
│ - Define API contracts (TypeBox) │
│ - Handle requests/responses │
│ - Thin handlers │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Service Layer │ Business logic
│ - Orchestrate operations │
│ - Implement business rules │
│ - Use repositories │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Repository Layer │ Data access
│ - CRUD operations │
│ - Custom queries │
│ - Extend base Repository │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Entity Layer │ Database schema
│ - Table definitions (Drizzle) │
│ - Type inference │
│ - Schema helpers │
└─────────────────────────────────────────┘Complete Example: Blog Post System
1. Entity Layer - Define database schema
// src/server/entities/posts.ts
import { pgTable, text } from 'drizzle-orm/pg-core';
import { id, timestamps } from '@spfn/core/db';
export const posts = pgTable('posts', {
id: id(),
title: text('title').notNull(),
slug: text('slug').notNull().unique(),
content: text('content').notNull(),
status: text('status', {
enum: ['draft', 'published']
}).notNull().default('draft'),
...timestamps(),
});
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;2. Repository Layer - Data access with custom methods
// src/server/entities/posts.ts (continued)
import { eq } from 'drizzle-orm';
import { Repository } from '@spfn/core/db';
export class PostRepository extends Repository<typeof posts> {
async findBySlug(slug: string) {
const results = await this.select()
.where(eq(this.table.slug, slug))
.limit(1);
return results[0] ?? null;
}
async findPublished() {
return this.select()
.where(eq(this.table.status, 'published'))
.orderBy(this.table.createdAt);
}
async create(data: NewPost) {
const [post] = await this.insert()
.values(data)
.returning();
return post;
}
}
// Export repository instance for reuse
export const postRepository = new PostRepository(posts);3. Routes Layer - HTTP API
// src/server/routes/posts/contracts.ts
import { Type } from '@sinclair/typebox';
export const createPostContract = {
method: 'POST' as const,
path: '/',
body: Type.Object({
title: Type.String(),
content: Type.String(),
}),
response: Type.Object({
id: Type.String(),
title: Type.String(),
slug: Type.String(),
}),
};
export const listPostsContract = {
method: 'GET' as const,
path: '/',
response: Type.Array(Type.Object({
id: Type.String(),
title: Type.String(),
slug: Type.String(),
})),
};// src/server/routes/posts/index.ts
import { createApp } from '@spfn/core/route';
import { Transactional } from '@spfn/core/db';
import { postRepository } from '../../entities/posts';
import { createPostContract, listPostsContract } from './contracts';
const app = createApp();
// POST /posts - Create new post (with transaction)
app.bind(createPostContract, Transactional(), async (c) => {
const body = await c.data();
// Generate slug from title
const slug = body.title.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
// Check if slug exists
const existing = await postRepository.findBySlug(slug);
if (existing) {
return c.json({ error: 'Post with this title already exists' }, 409);
}
// Create post
const post = await postRepository.create({
...body,
slug,
status: 'draft'
});
// ✅ Auto-commit on success, auto-rollback on error
return c.json(post, 201);
});
// GET /posts - List published posts (no transaction needed)
app.bind(listPostsContract, async (c) => {
const posts = await postRepository.findPublished();
return c.json(posts);
});
export default app;Why This Architecture?
✅ Separation of Concerns
- Each layer has a single responsibility
- Easy to locate and modify code
✅ Testability
- Test each layer independently
- Mock dependencies easily
✅ Reusability
- Services can be used by multiple routes
- Repositories can be shared across services
✅ Type Safety
- Types flow from Entity → Repository → Service → Route
- Full IDE autocomplete and error checking
✅ Maintainability
- Add features without breaking existing code
- Clear boundaries prevent coupling
Layer Responsibilities
| Layer | Responsibility | Examples |
|---|---|---|
| Entity | Define data structure | Schema, types, constraints |
| Repository | Data access | CRUD, custom queries, joins |
| Service | Business logic | Validation, orchestration, rules |
| Routes | HTTP interface | Contracts, request handling |
Best Practices
Entity Layer:
- ✅ Use schema helpers:
id(),timestamps() - ✅ Export inferred types:
Post,NewPost - ✅ Use TEXT with enum for status fields
Repository Layer:
- ✅ Extend
Repository<typeof table>for custom methods - ✅ Export repository instance from entity file:
export const repo = new MyRepository(table) - ✅ Add domain-specific query methods
- ✅ Return typed results
Routes Layer:
- ✅ Keep handlers thin (delegate to services)
- ✅ Define contracts with TypeBox
- ✅ Use
Transactional()middleware for write operations - ✅ Use
c.data()for validated input - ✅ Return
c.json()responses
Core Modules
📁 Routing
File-based routing with contract validation and type safety.
Key Features:
- Automatic route discovery (
index.ts,[id].ts,[...slug].ts) - Contract-based validation with TypeBox
- Type-safe request/response handling
- Method-level middleware control (skip auth per HTTP method)
🗄️ Database & Repository
Drizzle ORM integration with repository pattern and pagination.
Key Features:
- Repository pattern with automatic transaction handling
- Read/Write database separation
- Schema helpers:
id(),timestamps(),foreignKey()
🔄 Transactions
Automatic transaction management with async context propagation.
→ Read Transaction Documentation
Key Features:
- Auto-commit on success, auto-rollback on error
- AsyncLocalStorage-based context
- Transaction logging
💾 Cache
Redis integration with master-replica support.
⚠️ Error Handling
Custom error classes with unified HTTP responses.
🔐 Middleware
Request logging, CORS, and error handling middleware.
→ Read Middleware Documentation
🖥️ Server
Server configuration and lifecycle management.
📝 Logger
High-performance logging with multiple transports, sensitive data masking, and automatic validation.
Key Features:
- Adapter pattern (Pino for production, custom for full control)
- Sensitive data masking (passwords, tokens, API keys)
- File rotation (date and size-based) with automatic cleanup
- Configuration validation with clear error messages
- Multiple transports (Console, File, Slack, Email)
⚙️ Code Generation
Automatic code generation with pluggable generators and centralized file watching.
Key Features:
- Orchestrator pattern for managing multiple generators
- Built-in contract generator for type-safe API clients
- Configuration-based setup (
.spfnrc.jsonorpackage.json) - Watch mode integrated into
spfn dev - Extensible with custom generators
Module Exports
Main Export
import { startServer, createServer } from '@spfn/core';Routing
import { createApp, bind, loadRoutes } from '@spfn/core/route';
import type { RouteContext, RouteContract } from '@spfn/core/route';Database
import {
getDatabase,
Repository
} from '@spfn/core/db';Transactions
import {
Transactional,
getTransaction,
runWithTransaction
} from '@spfn/core/db';Cache
import { initRedis, getRedis, getRedisRead } from '@spfn/core';Logger
import { logger } from '@spfn/core';Client (for frontend)
import { ContractClient, createClient } from '@spfn/core/client';Environment Variables
# Database (required)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
# Database Read Replica (optional)
DATABASE_READ_URL=postgresql://user:pass@replica:5432/db
# Redis (optional)
REDIS_URL=redis://localhost:6379
REDIS_WRITE_URL=redis://master:6379 # Master-replica setup
REDIS_READ_URL=redis://replica:6379
# Server
PORT=8790
HOST=localhost
NODE_ENV=development
# Server Timeouts (optional, in milliseconds)
SERVER_TIMEOUT=120000 # Request timeout (default: 120000)
SERVER_KEEPALIVE_TIMEOUT=65000 # Keep-alive timeout (default: 65000)
SERVER_HEADERS_TIMEOUT=60000 # Headers timeout (default: 60000)
SHUTDOWN_TIMEOUT=30000 # Graceful shutdown timeout (default: 30000)
# Logger (optional)
LOGGER_ADAPTER=pino # pino | custom (default: pino)
LOGGER_FILE_ENABLED=true # Enable file logging (production only)
LOG_DIR=/var/log/myapp # Log directory (required when file logging enabled)Requirements
- Node.js >= 18
- Next.js 15+ with App Router (when using with CLI)
- PostgreSQL
- Redis (optional)
Testing
npm test # Run all tests
npm test -- route # Run route tests only
npm test -- --coverage # With coverageTest Coverage: 120+ tests across all modules
Documentation
Guides
- File-based Routing
- Database & Repository
- Transaction Management
- Redis Cache
- Error Handling
- Middleware
- Server Configuration
- Logger
- Code Generation
API Reference
- See module-specific README files linked above
License
MIT
Part of the SPFN Framework