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 | Node.js | ESM only
^5.0.0>=22基于Fastify的面向资源的后端框架。数据库无关、支持摇树优化、可用于生产环境。
依赖要求: Fastify | Node.js | 仅支持ESM
^5.0.0>=22Install
安装
bash
npm install @classytic/arc fastify
npm install @classytic/mongokit mongoose # MongoDB adapterbash
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 /:idAuthentication
身份认证
Auth uses a discriminated union with field:
typetypescript
// 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: falseDecorates: , ,
app.authenticateapp.optionalAuthenticateapp.authorize认证系统使用带字段的区分联合类型:
typetypescript
// 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.authenticateapp.optionalAuthenticateapp.authorizePermissions
权限管理
Function-based. A returns :
PermissionCheckboolean | { 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') }基于函数实现。返回:
PermissionCheckboolean | { 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
预设配置
| Preset | Routes Added | Controller Interface | Config |
|---|---|---|---|
| GET /deleted, POST /:id/restore | | |
| GET /slug/:slug | | |
| GET /tree, GET /:parent/children | | |
| none (middleware) | — | |
| none (middleware) | — | |
| none (middleware) | — | — |
typescript
presets: ['softDelete', { name: 'multiTenant', tenantField: 'organizationId' }]| 预设 | 新增路由 | 控制器接口 | 配置 |
|---|---|---|---|
| GET /deleted, POST /:id/restore | | |
| GET /slug/:slug | | |
| GET /tree, GET /:parent/children | | |
| 无(中间件) | — | |
| 无(中间件) | — | |
| 无(中间件) | — | — |
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: (default, zero-config) | (requires )
memorydistributedstores.queryCache: RedisCacheStoreResponse 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自然过期。
运行模式: (默认,零配置) | (需要)
memorydistributedstores.queryCache: RedisCacheStore响应头:
x-cache: HIT | STALE | MISSBaseController
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 — no manual setup needed:
eventPlugintypescript
// 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工厂会自动注册 — 无需手动配置:
eventPlugintypescript
// 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}.deletedFactory — 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,brandOperators: , , , , , , , , , ,
eqnegtgteltlteinninlikeregexexistsGET /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支持的操作符:, , , , , , , , , ,
eqnegtgteltlteinninlikeregexexistsError Classes
错误类
typescript
import { ArcError, NotFoundError, ValidationError, UnauthorizedError, ForbiddenError } from '@classytic/arc';
throw new NotFoundError('Product not found'); // 404typescript
import { ArcError, NotFoundError, ValidationError, UnauthorizedError, ForbiddenError } from '@classytic/arc';
throw new NotFoundError('产品不存在'); // 404CLI
命令行工具(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 doctorbash
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 doctorSubpath 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