@classytic/arc

中文文档
2.51.0发布于 2026/11/07MITTypeScriptES Module近 30 天下载 2740

Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable

原始使用文档
文档目录13 节
  1. Why arc
  2. Quick start
  3. Define a resource
  4. Resource shorthands
  5. Permissions
  6. Aggregations
  7. Authentication
  8. Subpath imports
  9. Testing
  10. CLI
  11. Documentation
  12. License
  13. Trademark

@classytic/arc

Sponsor

Database-agnostic resource framework for Fastify. One defineResource() call -> REST + auth + permissions + events + caching + OpenAPI + MCP tools.

Fastify 5+ | Node.js 22+ | ESM only

npm install @classytic/arc fastify

# Security defaults createApp() loads (each opt-out via `cors: false` etc.)
npm install @fastify/cors @fastify/helmet @fastify/rate-limit @fastify/under-pressure @fastify/sensible

# Storage adapter — pick one (kits ship their own adapters under `/adapter`)
npm install @classytic/mongokit mongoose             # MongoDB → @classytic/mongokit/adapter
# OR @classytic/sqlitekit drizzle-orm better-sqlite3 # → @classytic/sqlitekit/adapter
# OR @classytic/prismakit @prisma/client             # → @classytic/prismakit/adapter
# OR implement DataAdapter / RepositoryLike from @classytic/repo-core/adapter

Why arc

One call, full RESTdefineResource({ name, adapter, presets, permissions }) → GET /, GET /:id, POST /, PATCH /:id, DELETE /:id + custom routes + actions
DB-agnosticMongoose (@classytic/mongokit/adapter), Drizzle/SQLite (@classytic/sqlitekit/adapter), Prisma (@classytic/prismakit/adapter), or any RepositoryLike impl — swap backends without rewriting routes. Adapter contract lives in @classytic/repo-core/adapter; arc 2.12 ships zero kit-specific adapters.
Multi-tenant by defaultTenant-field auto-injected, scope-aware queries, per-org cache keys, elevation events.
Tree-shakable subpaths@classytic/arc/auth, /events, /cache, /mcp, /integrations/jobs — pay only for what you import.
MCP tools, freeResources auto-generate Model Context Protocol tools for AI agents. Same permissions, same field rules.

Quick start

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

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

// Fail fast on missing CORS env — silent `undefined` here drops to surprising
// browser defaults. Browser apps: declare an explicit allowlist (below).
// Server-to-server / API-key services: `cors: { origin: '*', credentials: false }`
// or `cors: false` to disable entirely (CORS is a browser-only concern).
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS;
if (!ALLOWED_ORIGINS) throw new Error('ALLOWED_ORIGINS env is required');

const app = await createApp({
  preset: 'production',
  resourcePrefix: '/api/v1',
  resources: await loadResources(import.meta.url),  // auto-discover *.resource.ts
  auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
  cors: { origin: ALLOWED_ORIGINS.split(','), credentials: true },
});

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

Resources can be a static array, an async factory (engine-bound), or auto-discovered from disk:

// Auto-discover (recommended for >5 resources)
resources: await loadResources(import.meta.url),

// Explicit list
resources: [productResource, orderResource],

// Async factory — runs after `bootstrap[]`, before route wiring
resources: async () => {
  const [catalog, flow] = await Promise.all([ensureCatalogEngine(), ensureFlowEngine()]);
  return loadResources(import.meta.url, { context: { catalog, flow } });
},

loadResources({ context }) threads engine handles into resources whose default export is (ctx) => defineResource(...). No parallel factory files, no exclude: [...] bookkeeping.


Define a resource

import { defineResource } from '@classytic/arc';
import { allowPublic, requireRoles, requireAuth } from '@classytic/arc/permissions';
import { createMongooseAdapter } from '@classytic/mongokit/adapter';
import { buildCrudSchemasFromModel } from '@classytic/mongokit';
import ProductModel from './product.model.js';
import productRepository from './product.repository.js';

export default defineResource({
  name: 'product',
  adapter: createMongooseAdapter({
    model: ProductModel,
    repository: productRepository,
    schemaGenerator: buildCrudSchemasFromModel,    // auto-derives CRUD schemas
  }),
  presets: ['softDelete', 'slugLookup', { name: 'multiTenant', tenantField: 'organizationId' }],
  permissions: {
    list: allowPublic(),
    get: allowPublic(),
    create: requireRoles(['admin']),
    update: requireRoles(['admin']),
    delete: requireRoles(['admin']),
  },
  schemaOptions: {
    fieldRules: {
      name: { minLength: 2, maxLength: 200 },
      sku: { pattern: '^[A-Z]{3}-\\d{3}$' },
      status: { enum: ['draft', 'active', 'archived'] },
      priceMode: { nullable: true },                          // accept null for round-trips
      organizationId: { systemManaged: true, preserveForElevated: true },
    },
    query: {
      allowedPopulate: ['category', 'createdBy'],             // populate whitelist
      filterableFields: { status: { type: 'string' } },
    },
  },
  cache: { staleTime: 30, gcTime: 300, tags: ['catalog'] },
  routes: [
    { method: 'GET', path: '/featured', handler: 'getFeatured', permissions: allowPublic() },
  ],
  actions: {
    approve: { handler: approveOrder, permissions: requireRoles(['admin']) },
  },
  // mcp: false,    // opt out of MCP tool generation for this resource
});

Auto-generates: GET /products, GET /products/:id, POST /products, PATCH /products/:id, DELETE /products/:id + softDelete adds GET /products/deleted, POST /products/:id/restore + slugLookup adds GET /products/slug/:slug + custom routes + POST /products/:id/action.

Resource shorthands

// Reference data — read-only list+get, fetch-all limits (1000), 5min/10min cache.
defineResource({ name: 'currency', adapter, referenceData: true });

// Service resource — custom routes only, no adapter, no auto-CRUD, no registry entry.
defineResource({
  name: 'health',
  customRoutesOnly: true,
  routes: [{ method: 'GET', path: '/ping', permissions: allowPublic(), handler: () => ({ ok: true }) }],
});

// Tune pagination caps inline (no custom queryParser needed).
defineResource({ name: 'pipeline', adapter, defaultLimit: 200, maxLimit: 500 });

A ?limit= above the cap is clamped to it and served, not rejected (changed in 2.33). Rejecting made the documented clamp unreachable and turned a benign over-ask into a 400 that callers commonly render as an empty list. The effective cap is carried in the OpenAPI parameter description, and the response's own limit reports what was applied.


Permissions

Function-based — RBAC, ABAC, ReBAC, or any combination.

import {
  allowPublic, requireAuth, requireRoles, requireOwnership,
  requireOrgMembership, requireOrgRole, requireServiceScope,
  requireScopeContext, requireOrgInScope,
  allOf, anyOf, when, denyAll,
  createDynamicPermissionMatrix,
} from '@classytic/arc/permissions';

permissions: {
  list: allowPublic(),
  get: requireAuth(),
  create: requireRoles(['admin', 'editor']),
  update: anyOf(requireOwnership('userId'), requireRoles(['admin'])),
  delete: allOf(requireAuth(), requireRoles(['admin'])),
}

Custom checks return { granted, reason?, filters?, scope? } — filters propagate into the repo query (row-level ABAC), scope stamps attributes downstream.


Aggregations

Add aggregations: { … } to a resource and arc registers GET /:prefix/aggregations/:name per entry. Each runs a portable $match → $group → $project → $sort → $limit pipeline against the kit's repo.aggregate(req, options) — same shape across mongokit / sqlitekit / prismakit, so dashboards work unchanged across backends.

import { defineResource, defineAggregation } from '@classytic/arc';

defineResource({
  name: 'transaction',
  adapter,
  presets: [multiTenantPreset({ tenantField: 'organizationId' })],
  permissions: { list: canViewRevenue() },

  aggregations: {
    byPaymentMethod: defineAggregation({
      groupBy: 'method',
      measures: { total: 'sum:amount', count: 'count' },
      sort: { total: -1 },
      cache: { staleTime: 60, swr: true, tags: ['revenue'] },
      permissions: canViewRevenue(),
    }),
    byDay: defineAggregation({
      dateBuckets: { day: { field: 'createdAt', interval: 'day' } },
      groupBy: 'flow',
      measures: { total: 'sum:amount', count: 'count' },
      requireDateRange: { field: 'createdAt', maxRangeDays: 365 },
      cache: { staleTime: 60, swr: true, tags: ['revenue'] },
      permissions: canViewRevenue(),
    }),
  },
});

Caller filters via query string compose with the declaration:

GET /api/transactions/aggregations/byPaymentMethod?status=verified
GET /api/transactions/aggregations/byDay?createdAt[gte]=2026-01-01&createdAt[lt]=2026-02-01

Tenant scope flows through repo.aggregate(req, options) — the kit's multi-tenant plugin handles type-coercion (string → ObjectId for mongokit fieldType: 'objectId', UUID/text for sqlitekit, etc.). Arc itself stays out of the filter slot because it's DB-agnostic. Safety guards on the declaration: requireFilters, requireDateRange { maxRangeDays }, maxGroups. SWR cache + tag invalidation tie aggregations to CRUD writes. Every aggregation auto-exports as an MCP tool with the same permissions and filter validation.


Authentication

Discriminated union on type:

// JWT (with optional revocation + custom token extractor)
auth: { type: 'jwt', jwt: { secret, expiresIn: '15m' } }

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

// Custom Fastify plugin
auth: { type: 'custom', plugin: myAuthPlugin }

// Disabled (e.g. internal services)
auth: false

Better Auth + arc resources over BA tables: the kit owns the bridge. @classytic/mongokit/better-auth ships createBetterAuthOverlay() (per-collection DataAdapter for defineResource) and registerBetterAuthStubs() (bulk stub models for populate()). Sqlitekit users hand-roll the Drizzle table — see auth-better-auth-overlay.md.


Subpath imports

Tree-shake by importing only the subpath you need:

SubpathPurpose
@classytic/arcdefineResource, BaseController, error classes
@classytic/repo-core/adapterAdapter contract types: DataAdapter, RepositoryLike, AdapterRepositoryInput, asRepositoryLike, isRepository. Imported from repo-core directly — arc deliberately does not re-export the contract to keep one source of truth.
@classytic/arc/factorycreateApp, loadResources, presets
@classytic/arc/authJWT + Better Auth adapters
@classytic/mongokit/better-authBA overlay for Mongoose: createBetterAuthOverlay, registerBetterAuthStubs (kit-owned)
@classytic/repo-core/better-authBA collection registry shared by every kit's overlay
@classytic/arc/permissionsAll permission helpers
@classytic/arc/scopeRequestScope accessors (isMember, isElevated, getOrgId, …)
@classytic/arc/cacheQueryCache, transports, plugin
@classytic/arc/eventsEvent plugin, MemoryEventTransport, outbox (event types live in @classytic/primitives/events)
@classytic/arc/events/redis · /redis-streamRedis Pub/Sub + Streams transports (opt-in)
@classytic/arc/pluginsHealth, request-id, versioning, tracing, response-cache
@classytic/arc/integrations/jobsBullMQ job dispatcher
@classytic/arc/integrations/websocketWebSocket integration
@classytic/arc/mcpModel Context Protocol tools + createMcpAuthFromBetterAuthApiKey (Better Auth API-key resolver), resolveToolCollisions
@classytic/arc/utilspipeUIMessageStreamToReply, UI_MESSAGE_STREAM_HEADERS (Vercel AI SDK → Fastify reply); errors, query parser, circuit breaker
@classytic/arc/testingcreateTestApp, expectArc, TestAuthProvider, createTestFixtures
@classytic/arc/typesType-only barrel (zero runtime cost)

Testing

import { createTestApp, expectArc } from '@classytic/arc/testing';
import productResource from './product.resource.js';

const ctx = await createTestApp({
  resources: [productResource],
  authMode: 'jwt',
  connectMongoose: true,           // in-memory Mongo + Mongoose connect
});
ctx.auth.register('admin', { user: { id: '1', role: 'admin' }, orgId: 'org-1' });

const res = await ctx.app.inject({
  method: 'POST',
  url: '/products',
  headers: ctx.auth.as('admin').headers,
  payload: { name: 'Widget' },
});
expectArc(res).ok().hidesField('password');

await ctx.close();

Three entry points: createTestApp (custom scenarios), createHttpTestHarness (~16 auto-generated CRUD/permission/validation tests per resource), runStorageContract (adapter conformance).


CLI

arc init my-api --mongokit --better-auth --ts    # scaffold a new project
arc generate resource product                    # generate a resource
arc generate resource product --mcp              # + MCP tools file
arc docs ./openapi.json --entry ./dist/index.js  # emit OpenAPI
arc introspect --entry ./dist/index.js           # introspect resources
arc doctor                                       # diagnose env

Documentation

  • Skill for AI agents: npx skills add classytic/arc — wires arc into Claude Code / agentic flows.
  • Concept reference: wiki/index.md — short, interlinked pages.
  • Guides: docs/ — getting-started, framework-extension, production-ops, testing, ecosystem.
  • Release notes: changelog/v2.md.

License

MIT

Trademark

The code is MIT-licensed. "Classytic", "arc", and the logos are trademarks of Classytic LLC and are not licensed under MIT — see TRADEMARK.md. Forks must be renamed; the license covers the code, not the brand.

版本与发布记录

@classytic/arc 共发布 124 个版本,首次发布于 2026/01/22;过去 12 个月发布了 124 个版本。

版本发布日期距今
2.51.0latest2026/11/072 天前
2.50.02026/11/045 天前
2.49.02026/11/036 天前
2.48.02026/10/2912 天前
2.47.02026/10/2813 天前
2.46.02026/10/2417 天前

关于 @classytic/arc 的常见问题

@classytic/arc 是什么?

Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable

如何安装 @classytic/arc?

在项目目录运行 npm install @classytic/arc,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:fastify(>=5.12.8)、@classytic/repo-core(>=0.38.0)、@classytic/primitives(>=0.37.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 arc,可以用 npx arc 直接运行。

@classytic/arc 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@classytic/arc 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@classytic/arc 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@classytic/arc 可以商用吗?

@classytic/arc 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@classytic/arc 的下载量有多少?

根据 npm 官方下载统计,@classytic/arc 在 2026/10/10 – 2026/11/08 这 30 天内被下载 2,740 次,日均约 91 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@classytic/arc 还在维护吗?

最新版本 2.51.0 发布于 2026/11/07(2 天前);至今共发布 124 个版本,其中过去 12 个月发布了 124 个;首次发布于 2026/01/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。