arc

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

@classytic/arc

@classytic/arc

Resource-oriented backend framework for Fastify. Database-agnostic, tree-shakable, production-ready.
Requires: Fastify
^5.0.0
| Node.js
>=22
| ESM only
基于Fastify的面向资源的后端框架。数据库无关、支持摇树优化、可用于生产环境。
依赖要求: Fastify
^5.0.0
| Node.js
>=22
| 仅支持ESM

Install

安装

bash
npm install @classytic/arc fastify
npm install @classytic/mongokit mongoose    # MongoDB adapter
bash
npm install @classytic/arc fastify
npm install @classytic/mongokit mongoose    # MongoDB适配器

Quick Start

快速开始

typescript
import { createApp } from '@classytic/arc/factory';
import mongoose from 'mongoose';

await mongoose.connect(process.env.DB_URI);

const app = await createApp({
  preset: 'production',
  auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
  cors: { origin: process.env.ALLOWED_ORIGINS?.split(',') },
});

await app.register(productResource.toPlugin());
await app.listen({ port: 8040, host: '0.0.0.0' });
typescript
import { createApp } from '@classytic/arc/factory';
import mongoose from 'mongoose';

await mongoose.connect(process.env.DB_URI);

const app = await createApp({
  preset: 'production',
  auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
  cors: { origin: process.env.ALLOWED_ORIGINS?.split(',') },
});

await app.register(productResource.toPlugin());
await app.listen({ port: 8040, host: '0.0.0.0' });

defineResource()

defineResource()

Single API to define a full REST resource:
typescript
import { defineResource, createMongooseAdapter, allowPublic, requireRoles } from '@classytic/arc';

const productResource = defineResource({
  name: 'product',
  adapter: createMongooseAdapter({ model: ProductModel, repository: productRepo }),
  controller: productController,  // optional — auto-created if omitted
  presets: ['softDelete', 'slugLookup', { name: 'multiTenant', tenantField: 'orgId' }],
  permissions: {
    list: allowPublic(),
    get: allowPublic(),
    create: requireRoles(['admin']),
    update: requireRoles(['admin']),
    delete: requireRoles(['admin']),
  },
  cache: { staleTime: 30, gcTime: 300, tags: ['catalog'] },
  additionalRoutes: [
    { method: 'GET', path: '/featured', handler: 'getFeatured', permissions: allowPublic(), wrapHandler: true },
  ],
});

await fastify.register(productResource.toPlugin());
// Auto-generates: GET /, GET /:id, POST /, PATCH /:id, DELETE /:id
通过单一API定义完整的REST资源:
typescript
import { defineResource, createMongooseAdapter, allowPublic, requireRoles } from '@classytic/arc';

const productResource = defineResource({
  name: 'product',
  adapter: createMongooseAdapter({ model: ProductModel, repository: productRepo }),
  controller: productController,  // 可选 — 若省略则自动创建
  presets: ['softDelete', 'slugLookup', { name: 'multiTenant', tenantField: 'orgId' }],
  permissions: {
    list: allowPublic(),
    get: allowPublic(),
    create: requireRoles(['admin']),
    update: requireRoles(['admin']),
    delete: requireRoles(['admin']),
  },
  cache: { staleTime: 30, gcTime: 300, tags: ['catalog'] },
  additionalRoutes: [
    { method: 'GET', path: '/featured', handler: 'getFeatured', permissions: allowPublic(), wrapHandler: true },
  ],
});

await fastify.register(productResource.toPlugin());
// 自动生成路由:GET /, GET /:id, POST /, PATCH /:id, DELETE /:id

Authentication

身份认证

Auth uses a discriminated union with
type
field:
typescript
// Arc JWT
auth: { type: 'jwt', jwt: { secret, expiresIn: '15m', refreshSecret, refreshExpiresIn: '7d' } }

// Better Auth (recommended for SaaS with orgs)
import { createBetterAuthAdapter } from '@classytic/arc/auth';
auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth, orgContext: true }) }

// Custom Fastify plugin (must decorate fastify.authenticate)
auth: { type: 'custom', plugin: myAuthPlugin }

// Custom function (decorates fastify.authenticate directly)
auth: { type: 'authenticator', authenticate: async (req, reply) => { ... } }

// Disabled
auth: false
Decorates:
app.authenticate
,
app.optionalAuthenticate
,
app.authorize
认证系统使用带
type
字段的区分联合类型:
typescript
// Arc JWT
auth: { type: 'jwt', jwt: { secret, expiresIn: '15m', refreshSecret, refreshExpiresIn: '7d' } }

// Better Auth(推荐用于带组织的SaaS)
import { createBetterAuthAdapter } from '@classytic/arc/auth';
auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth, orgContext: true }) }

// 自定义Fastify插件(必须装饰fastify.authenticate)
auth: { type: 'custom', plugin: myAuthPlugin }

// 自定义函数(直接装饰fastify.authenticate)
auth: { type: 'authenticator', authenticate: async (req, reply) => { ... } }

// 禁用认证
auth: false
添加的装饰器:
app.authenticate
,
app.optionalAuthenticate
,
app.authorize

Permissions

权限管理

Function-based. A
PermissionCheck
returns
boolean | { granted, reason?, filters? }
:
typescript
import {
  allowPublic, requireAuth, requireRoles, requireOwnership,
  requireOrgMembership, requireOrgRole, requireTeamMembership,
  allOf, anyOf, when, denyAll,
  createDynamicPermissionMatrix,
} from '@classytic/arc';

permissions: {
  list: allowPublic(),
  get: requireAuth(),
  create: requireRoles(['admin', 'editor']),
  update: anyOf(requireOwnership('userId'), requireRoles(['admin'])),
  delete: allOf(requireAuth(), requireRoles(['admin'])),
}
Custom permission:
typescript
const requirePro = (): PermissionCheck => async (ctx) => {
  if (!ctx.user) return { granted: false, reason: 'Auth required' };
  return { granted: ctx.user.plan === 'pro' };
};
Field-level permissions:
typescript
import { fields } from '@classytic/arc';

fields: {
  password: fields.hidden(),
  salary: fields.visibleTo(['admin', 'hr']),
  role: fields.writableBy(['admin']),
  email: fields.redactFor(['viewer'], '***'),
}
Dynamic ACL (DB-managed):
typescript
const acl = createDynamicPermissionMatrix({
  resolveRolePermissions: async (ctx) => aclService.getRoleMatrix(orgId),
  cacheStore: new RedisCacheStore({ client: redis, prefix: 'acl:' }),
});
permissions: { list: acl.canAction('product', 'read') }
基于函数实现。
PermissionCheck
返回
boolean | { granted, reason?, filters? }
typescript
import {
  allowPublic, requireAuth, requireRoles, requireOwnership,
  requireOrgMembership, requireOrgRole, requireTeamMembership,
  allOf, anyOf, when, denyAll,
  createDynamicPermissionMatrix,
} from '@classytic/arc';

permissions: {
  list: allowPublic(),
  get: requireAuth(),
  create: requireRoles(['admin', 'editor']),
  update: anyOf(requireOwnership('userId'), requireRoles(['admin'])),
  delete: allOf(requireAuth(), requireRoles(['admin'])),
}
自定义权限:
typescript
const requirePro = (): PermissionCheck => async (ctx) => {
  if (!ctx.user) return { granted: false, reason: '需要认证' };
  return { granted: ctx.user.plan === 'pro' };
};
字段级权限:
typescript
import { fields } from '@classytic/arc';

fields: {
  password: fields.hidden(),
  salary: fields.visibleTo(['admin', 'hr']),
  role: fields.writableBy(['admin']),
  email: fields.redactFor(['viewer'], '***'),
}
动态ACL(数据库管理):
typescript
const acl = createDynamicPermissionMatrix({
  resolveRolePermissions: async (ctx) => aclService.getRoleMatrix(orgId),
  cacheStore: new RedisCacheStore({ client: redis, prefix: 'acl:' }),
});
permissions: { list: acl.canAction('product', 'read') }

Presets

预设配置

PresetRoutes AddedController InterfaceConfig
softDelete
GET /deleted, POST /:id/restore
ISoftDeleteController
{ deletedField }
slugLookup
GET /slug/:slug
ISlugLookupController
{ slugField }
tree
GET /tree, GET /:parent/children
ITreeController
{ parentField }
ownedByUser
none (middleware)
{ ownerField }
multiTenant
none (middleware)
{ tenantField }
audited
none (middleware)
typescript
presets: ['softDelete', { name: 'multiTenant', tenantField: 'organizationId' }]
预设新增路由控制器接口配置
softDelete
GET /deleted, POST /:id/restore
ISoftDeleteController
{ deletedField }
slugLookup
GET /slug/:slug
ISlugLookupController
{ slugField }
tree
GET /tree, GET /:parent/children
ITreeController
{ parentField }
ownedByUser
无(中间件)
{ ownerField }
multiTenant
无(中间件)
{ tenantField }
audited
无(中间件)
typescript
presets: ['softDelete', { name: 'multiTenant', tenantField: 'organizationId' }]

QueryCache

QueryCache

TanStack Query-inspired server cache with stale-while-revalidate and auto-invalidation on mutations.
typescript
// Enable globally
const app = await createApp({ arcPlugins: { queryCache: true } });

// Per-resource config
defineResource({
  name: 'product',
  cache: {
    staleTime: 30,      // seconds fresh (no revalidation)
    gcTime: 300,         // seconds stale data kept (SWR window)
    tags: ['catalog'],   // cross-resource grouping
    invalidateOn: { 'category.*': ['catalog'] },  // event pattern → tag targets
    list: { staleTime: 60 },  // per-operation override
    byId: { staleTime: 10 },
  },
});
Auto-invalidation: POST/PATCH/DELETE bumps resource version. Old cached queries expire naturally via TTL.
Runtime modes:
memory
(default, zero-config) |
distributed
(requires
stores.queryCache: RedisCacheStore
)
Response header:
x-cache: HIT | STALE | MISS
受TanStack Query启发的服务端缓存,支持stale-while-revalidate策略,且在数据变更时自动失效。
typescript
// 全局启用
const app = await createApp({ arcPlugins: { queryCache: true } });

// 按资源配置
defineResource({
  name: 'product',
  cache: {
    staleTime: 30,      // 新鲜数据有效期(秒,期间不重新验证)
    gcTime: 300,         // 过期数据保留时长(秒,SWR窗口)
    tags: ['catalog'],   // 跨资源分组标签
    invalidateOn: { 'category.*': ['catalog'] },  // 事件模式 → 目标标签
    list: { staleTime: 60 },  // 按操作覆盖配置
    byId: { staleTime: 10 },
  },
});
自动失效: POST/PATCH/DELETE操作会更新资源版本。旧缓存查询会通过TTL自然过期。
运行模式:
memory
(默认,零配置) |
distributed
(需要
stores.queryCache: RedisCacheStore
响应头:
x-cache: HIT | STALE | MISS

BaseController

BaseController

typescript
import { BaseController } from '@classytic/arc';
import type { IRequestContext, IControllerResponse } from '@classytic/arc';

class ProductController extends BaseController<Product> {
  constructor() { super(productRepo); }

  async getFeatured(req: IRequestContext): Promise<IControllerResponse> {
    const products = await this.repository.getAll({ filters: { isFeatured: true } });
    return { success: true, data: products };
  }
}
IRequestContext:
{ params, query, body, user, headers, context, metadata, server }
IControllerResponse:
{ success, data?, error?, status?, meta?, headers? }
typescript
import { BaseController } from '@classytic/arc';
import type { IRequestContext, IControllerResponse } from '@classytic/arc';

class ProductController extends BaseController<Product> {
  constructor() { super(productRepo); }

  async getFeatured(req: IRequestContext): Promise<IControllerResponse> {
    const products = await this.repository.getAll({ filters: { isFeatured: true } });
    return { success: true, data: products };
  }
}
IRequestContext:
{ params, query, body, user, headers, context, metadata, server }
IControllerResponse:
{ success, data?, error?, status?, meta?, headers? }

Adapters (Database-Agnostic)

适配器(数据库无关)

typescript
// Mongoose
import { createMongooseAdapter } from '@classytic/arc';
const adapter = createMongooseAdapter({ model: ProductModel, repository: productRepo });

// Custom adapter — implement CrudRepository interface:
interface CrudRepository<TDoc> {
  getAll(params?): Promise<TDoc[] | PaginatedResult<TDoc>>;
  getById(id: string): Promise<TDoc | null>;
  create(data): Promise<TDoc>;
  update(id: string, data): Promise<TDoc | null>;
  delete(id: string): Promise<boolean>;
}
typescript
// Mongoose适配器
import { createMongooseAdapter } from '@classytic/arc';
const adapter = createMongooseAdapter({ model: ProductModel, repository: productRepo });

// 自定义适配器 — 实现CrudRepository接口:
interface CrudRepository<TDoc> {
  getAll(params?): Promise<TDoc[] | PaginatedResult<TDoc>>;
  getById(id: string): Promise<TDoc | null>;
  create(data): Promise<TDoc>;
  update(id: string, data): Promise<TDoc | null>;
  delete(id: string): Promise<boolean>;
}

Events

事件系统

The factory auto-registers
eventPlugin
— no manual setup needed:
typescript
// createApp() registers eventPlugin automatically (default: MemoryEventTransport)
const app = await createApp({
  stores: { events: new RedisEventTransport(redis) },  // optional, defaults to memory
  arcPlugins: {
    events: {                     // event plugin config (default: true, false to disable)
      logEvents: true,
      retry: { maxRetries: 3, backoffMs: 1000 },
    },
  },
});

await app.events.publish('order.created', { orderId: '123' });
await app.events.subscribe('order.*', async (event) => { ... });
CRUD events auto-emit:
{resource}.created
,
{resource}.updated
,
{resource}.deleted
.
工厂会自动注册
eventPlugin
— 无需手动配置:
typescript
// createApp()自动注册eventPlugin(默认:MemoryEventTransport)
const app = await createApp({
  stores: { events: new RedisEventTransport(redis) },  // 可选,默认使用内存
  arcPlugins: {
    events: {                     // 事件插件配置(默认:true,false表示禁用)
      logEvents: true,
      retry: { maxRetries: 3, backoffMs: 1000 },
    },
  },
});

await app.events.publish('order.created', { orderId: '123' });
await app.events.subscribe('order.*', async (event) => { ... });
CRUD操作会自动触发事件:
{resource}.created
,
{resource}.updated
,
{resource}.deleted

Factory — createApp()

工厂方法 — createApp()

typescript
const app = await createApp({
  preset: 'production',            // production | development | testing | edge
  runtime: 'memory',              // memory (default) | distributed
  auth: { type: 'jwt', jwt: { secret } },
  cors: { origin: ['https://myapp.com'] },
  helmet: true,                    // false to disable
  rateLimit: { max: 100 },         // false to disable
  arcPlugins: {
    events: true,                  // event plugin (default: true, false to disable)
    emitEvents: true,              // CRUD event emission (default: true)
    queryCache: true,              // server cache (default: false)
    sse: true,                     // SSE streaming (default: false)
    caching: true,                 // ETag + Cache-Control (default: false)
  },
  stores: {                        // required when runtime: 'distributed'
    events: new RedisEventTransport({ client: redis }),
    queryCache: new RedisCacheStore({ client: redis }),
  },
});
typescript
const app = await createApp({
  preset: 'production',            // production | development | testing | edge
  runtime: 'memory',              // memory(默认) | distributed
  auth: { type: 'jwt', jwt: { secret } },
  cors: { origin: ['https://myapp.com'] },
  helmet: true,                    // false表示禁用
  rateLimit: { max: 100 },         // false表示禁用
  arcPlugins: {
    events: true,                  // 事件插件(默认:true,false表示禁用)
    emitEvents: true,              // CRUD事件自动触发(默认:true)
    queryCache: true,              // 服务端缓存(默认:false)
    sse: true,                     // SSE流(默认:false)
    caching: true,                 // ETag + Cache-Control(默认:false)
  },
  stores: {                        // 当runtime为'distributed'时必填
    events: new RedisEventTransport({ client: redis }),
    queryCache: new RedisCacheStore({ client: redis }),
  },
});

Hooks

钩子

typescript
import { createHookSystem, beforeCreate, afterUpdate } from '@classytic/arc/hooks';

const hooks = createHookSystem();
beforeCreate(hooks, 'product', async (ctx) => { ctx.data.slug = slugify(ctx.data.name); });
afterUpdate(hooks, 'product', async (ctx) => { await invalidateCache(ctx.result._id); });
typescript
import { createHookSystem, beforeCreate, afterUpdate } from '@classytic/arc/hooks';

const hooks = createHookSystem();
beforeCreate(hooks, 'product', async (ctx) => { ctx.data.slug = slugify(ctx.data.name); });
afterUpdate(hooks, 'product', async (ctx) => { await invalidateCache(ctx.result._id); });

Pipeline

管道

typescript
import { guard, transform, intercept } from '@classytic/arc';

defineResource({
  pipe: {
    create: [
      guard('verified', async (ctx) => ctx.user?.verified === true),
      transform('inject', async (ctx) => { ctx.body.createdBy = ctx.user._id; }),
    ],
  },
});
typescript
import { guard, transform, intercept } from '@classytic/arc';

defineResource({
  pipe: {
    create: [
      guard('verified', async (ctx) => ctx.user?.verified === true),
      transform('inject', async (ctx) => { ctx.body.createdBy = ctx.user._id; }),
    ],
  },
});

Query Parsing

查询解析

GET /products?page=2&limit=20&sort=-createdAt&select=name,price
GET /products?price[gte]=100&status[in]=active,featured&search=keyword
GET /products?populate=category,brand
Operators:
eq
,
ne
,
gt
,
gte
,
lt
,
lte
,
in
,
nin
,
like
,
regex
,
exists
GET /products?page=2&limit=20&sort=-createdAt&select=name,price
GET /products?price[gte]=100&status[in]=active,featured&search=keyword
GET /products?populate=category,brand
支持的操作符:
eq
,
ne
,
gt
,
gte
,
lt
,
lte
,
in
,
nin
,
like
,
regex
,
exists

Error Classes

错误类

typescript
import { ArcError, NotFoundError, ValidationError, UnauthorizedError, ForbiddenError } from '@classytic/arc';
throw new NotFoundError('Product not found');  // 404
typescript
import { ArcError, NotFoundError, ValidationError, UnauthorizedError, ForbiddenError } from '@classytic/arc';
throw new NotFoundError('产品不存在');  // 404

CLI

命令行工具(CLI)

bash
arc init my-api --mongokit --better-auth --ts
arc generate resource product
arc docs ./openapi.json --entry ./dist/index.js
arc introspect --entry ./dist/index.js
arc doctor
bash
arc init my-api --mongokit --better-auth --ts
arc generate resource product
arc docs ./openapi.json --entry ./dist/index.js
arc introspect --entry ./dist/index.js
arc doctor

Subpath Imports

子路径导入

typescript
import { defineResource, BaseController, allowPublic } from '@classytic/arc';
import { createApp } from '@classytic/arc/factory';
import { MemoryCacheStore, RedisCacheStore, QueryCache } from '@classytic/arc/cache';
import { createBetterAuthAdapter, extractBetterAuthOpenApi } from '@classytic/arc/auth';
import type { ExternalOpenApiPaths } from '@classytic/arc/docs';
import { eventPlugin } from '@classytic/arc/events';
import { RedisEventTransport } from '@classytic/arc/events/redis';
import { healthPlugin, gracefulShutdownPlugin } from '@classytic/arc/plugins';
import { tracingPlugin } from '@classytic/arc/plugins/tracing';
import { auditPlugin } from '@classytic/arc/audit';
import { idempotencyPlugin } from '@classytic/arc/idempotency';
import { ssePlugin } from '@classytic/arc/plugins';
import { jobsPlugin } from '@classytic/arc/integrations/jobs';
import { websocketPlugin } from '@classytic/arc/integrations/websocket';
import { eventGatewayPlugin } from '@classytic/arc/integrations/event-gateway';
import { createHookSystem } from '@classytic/arc/hooks';
import { createTestApp } from '@classytic/arc/testing';
import { Type, ArcListResponse } from '@classytic/arc/schemas';
import { createStateMachine, CircuitBreaker } from '@classytic/arc/utils';
import { defineMigration } from '@classytic/arc/migrations';
import { isMember, isElevated, getOrgId } from '@classytic/arc/scope';
typescript
import { defineResource, BaseController, allowPublic } from '@classytic/arc';
import { createApp } from '@classytic/arc/factory';
import { MemoryCacheStore, RedisCacheStore, QueryCache } from '@classytic/arc/cache';
import { createBetterAuthAdapter, extractBetterAuthOpenApi } from '@classytic/arc/auth';
import type { ExternalOpenApiPaths } from '@classytic/arc/docs';
import { eventPlugin } from '@classytic/arc/events';
import { RedisEventTransport } from '@classytic/arc/events/redis';
import { healthPlugin, gracefulShutdownPlugin } from '@classytic/arc/plugins';
import { tracingPlugin } from '@classytic/arc/plugins/tracing';
import { auditPlugin } from '@classytic/arc/audit';
import { idempotencyPlugin } from '@classytic/arc/idempotency';
import { ssePlugin } from '@classytic/arc/plugins';
import { jobsPlugin } from '@classytic/arc/integrations/jobs';
import { websocketPlugin } from '@classytic/arc/integrations/websocket';
import { eventGatewayPlugin } from '@classytic/arc/integrations/event-gateway';
import { createHookSystem } from '@classytic/arc/hooks';
import { createTestApp } from '@classytic/arc/testing';
import { Type, ArcListResponse } from '@classytic/arc/schemas';
import { createStateMachine, CircuitBreaker } from '@classytic/arc/utils';
import { defineMigration } from '@classytic/arc/migrations';
import { isMember, isElevated, getOrgId } from '@classytic/arc/scope';

References (Progressive Disclosure)

参考文档(渐进式披露)

  • auth — JWT, Better Auth, API key auth, custom auth, multi-tenant
  • events — Domain events, transports, retry, auto-emission
  • integrations — BullMQ jobs, WebSocket, EventGateway, Streamline workflows
  • production — Health, audit, idempotency, tracing, SSE, QueryCache, OpenAPI
  • testing — Test app, mocks, data factories, in-memory MongoDB
  • auth — JWT、Better Auth、API密钥认证自定义认证、多租户
  • events — 领域事件、传输方式、重试机制、自动触发
  • integrations — BullMQ任务、WebSocket、EventGateway、工作流优化
  • production — 健康检查、审计、幂等性、链路追踪、SSE、QueryCache、OpenAPI
  • testing — 测试应用、模拟数据、数据工厂、内存MongoDB