Package Exports
- @spfn/core
- @spfn/core/cache
- @spfn/core/client
- @spfn/core/codegen
- @spfn/core/codegen/generators
- @spfn/core/db
- @spfn/core/env
- @spfn/core/errors
- @spfn/core/events
- @spfn/core/generators
- @spfn/core/logger
- @spfn/core/middleware
- @spfn/core/route
- @spfn/core/server
- @spfn/core/types
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 { findMany } 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;
// Use helper function directly - no Repository needed
const offset = (page - 1) * limit;
const result = await findMany(users, { limit, 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
Superfunction 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 helper functions or custom logic │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Data Access Layer │ Database operations
│ - Use helper functions (findOne, etc) │
│ - Custom queries with Drizzle │
│ - Domain-specific wrappers │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ 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. Data Access Layer - Helper functions with domain-specific wrappers
// src/server/repositories/posts.repository.ts
import { findOne, findMany, create as createHelper } from '@spfn/core/db';
import { eq, desc } from 'drizzle-orm';
import { posts, type Post, type NewPost } from '../entities/posts';
// Domain-specific wrappers using helper functions
export async function findPostBySlug(slug: string): Promise<Post | null> {
return findOne(posts, { slug });
}
export async function findPublishedPosts(): Promise<Post[]> {
return findMany(posts, {
where: { status: 'published' },
orderBy: desc(posts.createdAt)
});
}
export async function createPost(data: NewPost): Promise<Post> {
return createHelper(posts, data);
}
// Or use helper functions directly in routes for simple cases
// const post = await findOne(posts, { id: 1 });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 { ConflictError } from '@spfn/core/errors';
import { findPostBySlug, createPost, findPublishedPosts } from '../../repositories/posts.repository';
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 findPostBySlug(slug);
if (existing) {
throw new ConflictError('Post with this title already exists', { slug });
}
// Create post
const post = await createPost({
...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 findPublishedPosts();
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
- Data access functions can be shared across services
✅ Type Safety
- Types flow from Entity → Data Access → 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 |
| Data Access | Database operations | Helper functions, 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
Data Access Layer:
- ✅ Use helper functions for simple CRUD:
findOne(),create(), etc. - ✅ Create domain-specific wrappers in
src/server/repositories/*.repository.ts - ✅ Export functions (not classes):
export async function findPostBySlug() - ✅ Use object-based where for simple queries:
{ id: 1 } - ✅ Use SQL-based where for complex queries:
and(eq(...), gt(...)) - ✅ Full TypeScript type inference from table schemas
Routes Layer:
- ✅ Keep handlers thin (delegate to services/data access)
- ✅ 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
Drizzle ORM integration with type-safe helper functions and automatic transaction handling.
Key Features:
- Helper functions for type-safe CRUD operations
- Automatic transaction handling and read/write separation
- Schema helpers:
id(),timestamps(),foreignKey() - Hybrid where clause support (objects or SQL)
- Function schema auto-discovery (see below)
📦 Function Schema Discovery
Automatic discovery of database schemas from Superfunction ecosystem functions.
→ Read Database Manager Documentation
Key Features:
- Zero-config schema discovery from
@spfn/*functions - Functions declare schemas via
package.json - No hard dependencies between functions
- Efficient scanning (direct dependencies only)
- Function-specific migration support
How it works:
Functions declare their schemas in package.json:
{
"name": "@spfn/cms",
"spfn": {
"schemas": ["./dist/entities/*.js"],
"setupMessage": "📚 Setup guide..."
}
}Superfunction automatically discovers and merges these schemas during migration generation:
import { getDrizzleConfig } from '@spfn/core'
// Auto-discovers all function schemas
const config = getDrizzleConfig()Install functions with automatic DB setup:
pnpm spfn add @spfn/cms
# ✅ Installs function
# ✅ Generates migrations
# ✅ Applies migrations
# ✅ Shows setup guideCreate your own Superfunction packages:
// 1. Define entities
export const myTable = pgTable('my_table', { ... })
// 2. Add to package.json
{
"spfn": {
"schemas": ["./dist/entities/*.js"]
}
}
// 3. Users install with one command
// pnpm spfn add @yourcompany/spfn-plugin🔄 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,
findOne,
findMany,
create,
createMany,
updateOne,
updateMany,
deleteOne,
deleteMany,
count
} 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 & Helper Functions
- 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 Superfunction Framework