JSPM

  • Created
  • Published
  • Downloads 379
  • Score
    100M100P100Q95179F
  • License MIT

SPFN Framework Core - File-based routing, transactions, repository pattern

Package Exports

  • @spfn/core/cache
  • @spfn/core/client
  • @spfn/core/codegen
  • @spfn/core/config
  • @spfn/core/db
  • @spfn/core/env
  • @spfn/core/env/loader
  • @spfn/core/errors
  • @spfn/core/event
  • @spfn/core/event/sse
  • @spfn/core/event/sse/client
  • @spfn/core/job
  • @spfn/core/logger
  • @spfn/core/middleware
  • @spfn/core/nextjs
  • @spfn/core/nextjs/server
  • @spfn/core/route
  • @spfn/core/route/types
  • @spfn/core/server

Readme

@spfn/core

Type-safe Node.js backend framework built on Hono + Drizzle ORM.

npm version License: MIT TypeScript

Beta Release: Core APIs are stable but may have minor changes before 1.0.

Installation

pnpm add @spfn/core

Quick Start

1. Define Entity

// src/server/entities/users.ts
import { pgTable, text, boolean } from 'drizzle-orm/pg-core';
import { id, timestamps } from '@spfn/core/db';

export const users = pgTable('users', {
    id: id(),
    email: text('email').notNull().unique(),
    name: text('name').notNull(),
    isActive: boolean('is_active').notNull().default(true),
    ...timestamps()
});

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

2. Create Repository

// src/server/repositories/user.repository.ts
import { BaseRepository } from '@spfn/core/db';
import { users, type User, type NewUser } from '../entities/users';

export class UserRepository extends BaseRepository
{
    async findById(id: string): Promise<User | null>
    {
        return this._findOne(users, { id });
    }

    async findByEmail(email: string): Promise<User | null>
    {
        return this._findOne(users, { email });
    }

    async findAll(): Promise<User[]>
    {
        return this._findMany(users);
    }

    async create(data: NewUser): Promise<User>
    {
        return this._create(users, data);
    }

    async update(id: string, data: Partial<NewUser>): Promise<User | null>
    {
        return this._updateOne(users, { id }, data);
    }

    async delete(id: string): Promise<User | null>
    {
        return this._deleteOne(users, { id });
    }
}

3. Define Routes

// src/server/routes/users.ts
import { route } from '@spfn/core/route';
import { Transactional } from '@spfn/core/db';
import { Type } from '@sinclair/typebox';
import { UserRepository } from '../repositories/user.repository';

const userRepo = new UserRepository();

export const getUsers = route.get('/users')
    .handler(async () => {
        return userRepo.findAll();
    });

export const getUser = route.get('/users/:id')
    .input({
        params: Type.Object({ id: Type.String() })
    })
    .handler(async (c) => {
        const { params } = await c.data();
        const user = await userRepo.findById(params.id);

        if (!user)
        {
            throw new Error('User not found');
        }

        return user;
    });

export const createUser = route.post('/users')
    .input({
        body: Type.Object({
            email: Type.String({ format: 'email' }),
            name: Type.String({ minLength: 1 })
        })
    })
    .use([Transactional()])
    .handler(async (c) => {
        const { body } = await c.data();
        return userRepo.create(body);
    });

export const updateUser = route.patch('/users/:id')
    .input({
        params: Type.Object({ id: Type.String() }),
        body: Type.Object({
            email: Type.Optional(Type.String({ format: 'email' })),
            name: Type.Optional(Type.String({ minLength: 1 }))
        })
    })
    .use([Transactional()])
    .handler(async (c) => {
        const { params, body } = await c.data();
        const user = await userRepo.update(params.id, body);

        if (!user)
        {
            throw new Error('User not found');
        }

        return user;
    });

export const deleteUser = route.delete('/users/:id')
    .input({
        params: Type.Object({ id: Type.String() })
    })
    .use([Transactional()])
    .handler(async (c) => {
        const { params } = await c.data();
        const user = await userRepo.delete(params.id);

        if (!user)
        {
            throw new Error('User not found');
        }

        return { success: true };
    });

4. Configure Server

// src/server/server.config.ts
import { defineServerConfig, defineRouter } from '@spfn/core/server';
import * as userRoutes from './routes/users';

const appRouter = defineRouter({
    ...userRoutes
});

export default defineServerConfig()
    .port(8790)
    .routes(appRouter)
    .build();

export type AppRouter = typeof appRouter;

5. Start Server

// src/server/index.ts
import { startServer } from '@spfn/core/server';

await startServer();

6. Environment Variables

# .env
DATABASE_URL=postgresql://localhost:5432/mydb
PORT=8790

Architecture Overview

┌─────────────────────────────────────────┐
│            Routes Layer                  │  API endpoints + validation
│  route.get('/users/:id').handler(...)   │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│         Repository Layer                 │  Business logic + data access
│  class UserRepository extends Base...   │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│           Entity Layer                   │  Database schema (Drizzle)
│  pgTable('users', { id, email, ... })   │
└─────────────────────────────────────────┘

Key Principles:

  • Route: Thin layer, input validation only
  • Repository: All business logic and DB access
  • Entity: Schema definition only, no logic

Directory Structure

src/server/
├── entities/              # Database schema
│   ├── users.ts
│   └── index.ts
├── repositories/          # Data access + business logic
│   ├── user.repository.ts
│   └── index.ts
├── routes/                # API routes
│   ├── users.ts
│   └── index.ts
├── server.config.ts       # Server configuration
└── index.ts               # Entry point

Core Concepts

Route Definition

import { route } from '@spfn/core/route';
import { Type } from '@sinclair/typebox';

// GET with params and query
route.get('/users/:id')
    .input({
        params: Type.Object({ id: Type.String() }),
        query: Type.Object({ include: Type.Optional(Type.String()) })
    })
    .handler(async (c) => {
        const { params, query } = await c.data();
        // params.id, query.include are fully typed
    });

// POST with body
route.post('/users')
    .input({
        body: Type.Object({
            email: Type.String(),
            name: Type.String()
        })
    })
    .handler(async (c) => {
        const { body } = await c.data();
        // body.email, body.name are fully typed
    });

Repository Pattern

import { BaseRepository } from '@spfn/core/db';

export class UserRepository extends BaseRepository
{
    // Protected helpers available:
    // _findOne, _findMany, _create, _createMany
    // _updateOne, _updateMany, _deleteOne, _deleteMany
    // _count, _upsert

    async findActive()
    {
        return this._findMany(users, {
            where: { isActive: true },
            orderBy: desc(users.createdAt),
            limit: 10
        });
    }
}

Transaction

import { Transactional } from '@spfn/core/db';

// Middleware-based (recommended)
route.post('/users')
    .use([Transactional()])
    .handler(async (c) => {
        // Auto commit on success
        // Auto rollback on error
    });

// Manual control
import { runWithTransaction } from '@spfn/core/db';

await runWithTransaction(async () => {
    await userRepo.create(userData);
    await profileRepo.create(profileData);
});

Schema Helpers

import {
    id,              // bigserial primary key
    uuid,            // uuid primary key
    timestamps,      // createdAt, updatedAt
    foreignKey,      // required FK with cascade
    optionalForeignKey,  // nullable FK
    softDelete,      // deletedAt, deletedBy
    enumText,        // type-safe enum
    typedJsonb       // type-safe JSONB
} from '@spfn/core/db';

export const posts = pgTable('posts', {
    id: id(),
    title: text('title').notNull(),
    authorId: foreignKey('author', () => users.id),
    status: enumText('status', ['draft', 'published']).default('draft'),
    metadata: typedJsonb<{ views: number }>('metadata'),
    ...timestamps(),
    ...softDelete()
});

Module Documentation

Module Description
Route Route definition, validation, response patterns
Database Connection, helpers, transactions
Entity Schema definition, column helpers
Repository BaseRepository, CRUD patterns
Middleware Named middleware, skip control
Server Configuration, lifecycle hooks
Errors Error types, handling patterns
Environment Type-safe environment variables
Codegen API client generation
Cache Redis caching
Event Event system
Job Background jobs
Logger Logging
Next.js Next.js integration, RPC proxy

CLI Commands

# Migration
npx spfn db generate    # Generate migration
npx spfn db migrate     # Apply migration

# Development
pnpm spfn:dev           # Start dev server (auto codegen)

# API Client Generation
pnpm spfn codegen run

# Database Studio
pnpm drizzle-kit studio

License

MIT