@spfn/core
Version:
SPFN Framework Core - File-based routing, transactions, repository pattern
561 lines (440 loc) • 16 kB
Markdown
# @spfn/core
> Core framework for building type-safe backend APIs with Next.js and Hono
[](https://www.npmjs.com/package/@spfn/core)
[](https://opensource.org/licenses/MIT)
[](https://www.typescriptlang.org/)
> ⚠️ **Alpha Release**: SPFN is currently in alpha. APIs may change. Use `@alpha` tag for installation.
## Installation
**Recommended: Create New Project**
```bash
npx spfn@alpha create my-app
```
**Add to Existing Next.js Project**
```bash
cd your-nextjs-project
npx spfn@alpha init
```
**Manual Installation**
```bash
npm install @spfn/core hono drizzle-orm postgres @sinclair/typebox
```
## Quick Start
### 1. Define a Contract
```typescript
// 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
```typescript
// 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
```bash
npm run spfn:dev
# Server starts on http://localhost:8790
```
## Architecture 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
```typescript
// 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
```typescript
// 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
```typescript
// 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(),
})),
};
```
```typescript
// 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.
**[→ Read Routing Documentation](./src/route/README.md)**
**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.
**[→ Read Database Documentation](./src/db/README.md)**
**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](./src/db/manager/README.md)**
**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`:
```json
{
"name": "@spfn/cms",
"spfn": {
"schemas": ["./dist/entities/*.js"],
"setupMessage": "📚 Setup guide..."
}
}
```
Superfunction automatically discovers and merges these schemas during migration generation:
```typescript
import { getDrizzleConfig } from '@spfn/core'
// Auto-discovers all function schemas
const config = getDrizzleConfig()
```
**Install functions with automatic DB setup:**
```bash
pnpm spfn add @spfn/cms
# ✅ Installs function
# ✅ Generates migrations
# ✅ Applies migrations
# ✅ Shows setup guide
```
**Create your own Superfunction packages:**
```typescript
// 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](./src/db/docs/transactions.md)**
**Key Features:**
- Auto-commit on success, auto-rollback on error
- AsyncLocalStorage-based context
- Transaction logging
### 💾 Cache
Redis integration with master-replica support.
**[→ Read Cache Documentation](./src/cache/README.md)**
### ⚠️ Error Handling
Custom error classes with unified HTTP responses.
**[→ Read Error Documentation](./src/errors/README.md)**
### 🔐 Middleware
Request logging, CORS, and error handling middleware.
**[→ Read Middleware Documentation](./src/middleware/README.md)**
### 🖥️ Server
Server configuration and lifecycle management.
**[→ Read Server Documentation](./src/server/README.md)**
### 📝 Logger
High-performance logging with multiple transports, sensitive data masking, and automatic validation.
**[→ Read Logger Documentation](./src/logger/README.md)**
**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.
**[→ Read Codegen Documentation](./src/codegen/README.md)**
**Key Features:**
- Orchestrator pattern for managing multiple generators
- Built-in contract generator for type-safe API clients
- Configuration-based setup (`.spfnrc.json` or `package.json`)
- Watch mode integrated into `spfn dev`
- Extensible with custom generators
## Module Exports
### Main Export
```typescript
import { startServer, createServer } from '@spfn/core';
```
### Routing
```typescript
import { createApp, bind, loadRoutes } from '@spfn/core/route';
import type { RouteContext, RouteContract } from '@spfn/core/route';
```
### Database
```typescript
import {
getDatabase,
findOne,
findMany,
create,
createMany,
updateOne,
updateMany,
deleteOne,
deleteMany,
count
} from '@spfn/core/db';
```
### Transactions
```typescript
import {
Transactional,
getTransaction,
runWithTransaction
} from '@spfn/core/db';
```
### Cache
```typescript
import { initRedis, getRedis, getRedisRead } from '@spfn/core';
```
### Logger
```typescript
import { logger } from '@spfn/core';
```
### Client (for frontend)
```typescript
import { ContractClient, createClient } from '@spfn/core/client';
```
## Environment Variables
```bash
# 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
```bash
npm test # Run all tests
npm test -- route # Run route tests only
npm test -- --coverage # With coverage
```
**Test Coverage:** 120+ tests across all modules
## Documentation
### Guides
- [File-based Routing](./src/route/README.md)
- [Database & Helper Functions](./src/db/README.md)
- [Transaction Management](./src/db/docs/transactions.md)
- [Redis Cache](./src/cache/README.md)
- [Error Handling](./src/errors/README.md)
- [Middleware](./src/middleware/README.md)
- [Server Configuration](./src/server/README.md)
- [Logger](./src/logger/README.md)
- [Code Generation](./src/codegen/README.md)
### API Reference
- See module-specific README files linked above
## License
MIT
---
Part of the [Superfunction Framework](https://github.com/spfn/spfn)