autocrud-core
v0.1.4
Published
Auto-generate CRUD REST + GraphQL endpoints from schemas (MVP1)
Maintainers
Readme
Autocrud
Plug-and-play Node.js library that auto-generates CRUD REST + GraphQL endpoints and a functional API from simple JSON schemas. Ships with multiple database adapters, transforms, caching, joins, observability, and hot-reload.
Core ideas
- Define schemas as JSON files (or generate them). No code for CRUD.
- Get three ways to use them: Functions, REST, and GraphQL.
- Swap storage by adapter: File, SQLite, Postgres, or MongoDB.
- Opt-in transforms, caching, joins, observability, and live schema reload.
Installation
npm install autocrud-core
# Optional: for SQLite adapter
npm install better-sqlite3Requirements: Node 18+
Usage
- ESM (recommended):
import { buildAutoCRUD, generateFunctions } from "autocrud-core";- CommonJS (compat):
const { buildAutoCRUD, generateFunctions } = require("autocrud-core");Package format
- ESM-first with CJS compatibility
- ESM entry:
dist/index.jsviaexports.import - CJS entry:
dist/index.cjsviaexports.require - Types:
dist/index.d.ts
- ESM entry:
Quick start (pick one)
- Functions only (no HTTP):
npm run demo- See
examples/demo.ts
- REST + GraphQL server:
npm run dev:server- Uses
examples/server.ts+examples/config.ts
- Define schemas (JSON)
Example src/schemas/user.json
{
"name": "user",
"primaryKey": { "name": "id", "auto": true, "strategy": "uuid", "type": "string" },
"timestamps": true,
"fields": {
"id": { "type": "string", "required": true },
"email": { "type": "string", "required": true },
"password": { "type": "string", "required": true },
"role": { "type": "string" }
}
}Example src/schemas/product_seq.json (numeric sequence PK)
{
"name": "product",
"primaryKey": { "name": "id", "auto": true, "strategy": "sequence", "type": "number", "start": 100, "step": 1 },
"timestamps": true,
"fields": {
"id": { "type": "number", "required": true },
"name": { "type": "string", "required": true },
"price": { "type": "number", "required": true }
}
}- Create a config
Simplest config (file adapter) — see examples/config.ts for a fuller version.
export default {
server: { port: 4000, basePath: "/api", graphqlPath: "/graphql" },
database: { type: "file", url: "./data" },
schemas: {
user: { file: "./src/schemas/user.json", ops: { delete: false } },
product: { file: "./src/schemas/product_seq.json" }
},
joins: {
userOrders: { base: "user", relations: [ { schema: "order", localField: "id", foreignField: "userId", as: "orders", type: "left" } ] }
},
cache: { enabled: true, ttl: 60 },
functional: { enabled: true }
}- Use it: functions first
import { generateFunctions } from "autocrud-core";
const { functions, stop } = await generateFunctions(config);
await functions.user.insert({ email: "[email protected]", password: "p" });
const list = await functions.user.find({ pagination: { limit: 10 } });
const joined = await functions.join.userOrders({ filter: { id: { eq: list[0].id } } });
await stop();Or: start REST + GraphQL
import { buildAutoCRUD } from "autocrud-core";
const orch = await buildAutoCRUD(config);
await orch.start();Server port behavior
- If you pass an existing Express app via
server.existingApp, Autocrud mounts routes and does not calllisten(); your app controls the port. - Otherwise, Autocrud creates an HTTP server:
- Uses
process.env.PORTif set; otherwiseserver.port(default 4000). - On EADDRINUSE, fallback is configurable via
server.portFallback:"error": throw on conflict"increment"(default): tryport+1up toserver.maxPortRetries(default 10)"auto": bind to an ephemeral port chosen by the OS
- The chosen
actualPortis exposed in/autocurd-infoand used in/autocurd-listsample curl.
- Uses
HTTP overview
- REST per schema (respects ops flags):
- GET
/:schemalist (filter via query) - GET
/:schema/:idby id - POST
/:schemacreate - PATCH
/:schema/:idupdate - DELETE
/:schema/:iddelete - Example:
curl -s "http://localhost:4000/api/user?limit=10"
- GET
- REST joins:
- GET
/api/join/<name>?limit=&offset=&sort=&dir=&relations=<json>&<alias>Filter=<json>&<alias>SortField=&<alias>SortDir=&<alias>Limit=&<alias>Offset=
- GET
- GraphQL (auto-generated):
- Query:
user(id: ID!): User - Typed filters:
userList(filter: UserFilter, pagination: PaginationInput, sort: UserSort): [User!]! - Mutations:
createUser(input),updateUser(id, input),deleteUser(id) - Joins:
join<Name>List(filter, limit, offset, <alias>Filter, <alias>SortField, <alias>SortDir, <alias>Limit, <alias>Offset)
- Query:
Configuration (table)
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| server.port | number | 4000 | HTTP port when creating a new Express app. |
| server.basePath | string | /api | Base path for REST and joins. |
| server.graphqlPath | string | /graphql | Path for GraphQL endpoint. |
| server.restEnabled | boolean | true | Mount REST endpoints. |
| server.graphqlEnabled | boolean | true | Mount GraphQL endpoint. |
| server.loggingEnabled | boolean | true | Structured request logs (Pino). |
| server.logLevel | string | info | Pino log level. |
| server.tracingEnabled | boolean | true | Adds X-Request-Id and logs it. |
| server.metricsEnabled | boolean | true | Prometheus metrics middleware. |
| server.metricsPath | string | /autocurd-metrics | Metrics endpoint path. |
| server.healthEnabled | boolean | true | Enables /autocurd-health. |
| server.infoEnabled | boolean | true | Enables /autocurd-info. |
| server.listEnabled | boolean | true | Enables /autocurd-list. |
| server.schemaHotReloadEnabled | boolean | true | Watch schemas and hot-reload routers/SDL. |
| database.type | enum | file | One of file|sqlite|postgres|mongodb. |
| database.url | string | ./data | File dir (file), file path (sqlite), conn string (pg/mongo). |
| cache.enabled | boolean | true | Read-through cache for lists/items. |
| cache.ttl | number | 60 | TTL seconds. |
| functional.enabled | boolean | true | Generate functions API. |
| schemas.<name>.file | string | — | Path to schema JSON (required). |
| schemas.<name>.transform.enabled | boolean | false | Enable field-level before/after transforms. |
| schemas.<name>.ops.create | boolean | true | Enable create (REST/GraphQL). |
| schemas.<name>.ops.read | boolean | true | Enable list (REST/GraphQL). |
| schemas.<name>.ops.readOne | boolean | true | Enable get by id (REST/GraphQL). |
| schemas.<name>.ops.update | boolean | true | Enable update (REST/GraphQL). |
| schemas.<name>.ops.delete | boolean | true | Enable delete (REST/GraphQL). |
| joins.<name> | object | — | { base, relations:[{ schema, localField, foreignField, as, type }] }. |
Protocol matrix (REST, GraphQL, Functions)
| Capability | REST | GraphQL | Functions |
| --- | --- | --- | --- |
| List | GET /api/:schema?limit&offset&sort&dir | <schema>List(filter, pagination, sort) | functions[schema].find({ filter, pagination, sort }) |
| Get by id | GET /api/:schema/:id | <schema>(id: ID!) | functions[schema].findById(id) |
| Create | POST /api/:schema | create<Schema>(input) | functions[schema].insert(doc) |
| Update | PATCH /api/:schema/:id | update<Schema>(id, input) | functions[schema].update(id, patch) |
| Delete | DELETE /api/:schema/:id | delete<Schema>(id) | functions[schema].delete(id) |
| Join list | GET /api/join/<name> | join<Name>List(filter, pagination, sort, <alias>Filter, <alias>Sort, <alias>Pagination) | functions.join.<name>({ filter, relations:{ alias:{ filter, sort, pagination }}}) |
| Typed filters | Basic (query params) | Yes (per-schema filter inputs) | Yes (filter object) |
| Sort | sort/dir query | sort: <Schema>Sort | sort option |
| Pagination | limit/offset query | pagination: PaginationInput | pagination option |
| ETag 304 | Yes on GET | N/A | N/A |
GraphQL typed filter, sort, pagination (reference)
| Input | Fields |
| --- | --- |
| PaginationInput | limit: Int, offset: Int |
| SortDirection | asc, desc |
| StringFilter | eq, contains, in |
| NumberFilter | eq, gt, gte, lt, lte, in |
| BooleanFilter | eq |
| DateFilter | eq, gt, gte, lt, lte |
| <Schema>Filter | Per-field scalar filters (e.g., email: StringFilter) |
| <Schema>SortField | Enum of schema fields |
| <Schema>Sort | field: <Schema>SortField, direction: SortDirection |
Schema features
Features (table)
| Feature | Default | Where | Notes |
| --- | --- | --- | --- |
| Field types | — | Schema | string, number, boolean, date (ISO), json |
| Constraints | — | Schema | required, default, maxLength, min, max |
| Primary key | id, uuid | Schema/DB | Object form supports { name, auto, strategy: uuid|sequence, start, step, type } |
| PK by adapter | — | DB | File: uuid/sequence; SQLite: AUTOINCREMENT; Postgres: identity; Mongo: uuid only |
| Timestamps | true | Schema | Auto createdAt/updatedAt if timestamps: true |
| Validation | On | REST/GraphQL | AJV validators for create/update per schema |
| Transforms | Off | All ops | beforeSave/afterRead per field in config |
| Caching | On (ttl=60s) | Reads | List/item cache, id-level invalidation, ETags for REST GET |
| Joins | — | All | Config-driven, relation-level filter/sort/pagination |
| Hot reload | On | Server | Watches schema dirs; rebuilds REST/joins/GraphQL |
| Observability | On | Server | Logs, metrics, tracing; utility endpoints enabled by default |
| Docs endpoints | On | Server | /autocurd-openapi.json, /autocurd-sdl |
Validation
- AJV-based per-schema input validation for REST/GraphQL (create/update)
- Dates are strings (ISO recommended)
Transforms
- Per-schema
transformhooks in config:beforeSave: applied on insert/update, per fieldafterRead: applied on reads, per field (e.g., hidepassword)
Caching
- In-memory TTL cache for reads; id-level keys + list keys
- Automatic invalidation on writes (by schema and id)
- REST GETs return ETag; send
If-None-Matchfor 304
Database connections (samples)
- File (default for demos)
{ type: "file", url: "./data" }
- SQLite (requires better-sqlite3)
{ type: "sqlite", url: "./data/dev.db" }
- Postgres
{ type: "postgres", url: "postgres://user:pass@host:5432/dbname" }
- MongoDB
{ type: "mongodb", url: "mongodb://localhost:27017/mydb" }
Joins
- Define in config under
joins:{ base, relations: [{ schema, localField, foreignField, as, type }] }
- Call via functions:
functions.join.<name>({ filter, sort, pagination, relations: { alias: { filter, sort, pagination }}}) - REST joins endpoint (see above) and GraphQL nested types (
Join<Name>withbase+ relation arrays)
Adapters
- File (default for demos)
- Single-process in-memory index of
data/<schema>.json - Atomic writes via
.tmp+.bak; recovers from.bakon parse errors - Great for prototyping, light writes, local dev
- Single-process in-memory index of
- SQLite (better-sqlite3, optional dep)
- Single-file DB, strong concurrency, identity PK
- Postgres
- Pool-based adapter, identity PK, JSONB, parameterized queries
- MongoDB
- Official driver, uses
_idfrom your PK when present
- Official driver, uses
Observability
- Structured logs (Pino) with request IDs
- Prometheus metrics
- Served under the REST base path:
<basePath><metricsPath>(e.g.,/api/autocurd-metrics) - Backward-compat route also available at root:
/autocurd-metrics autocrud_requests_total{method,route,code}autocrud_request_duration_ms_sum|count{route}
- Served under the REST base path:
- Tracing via
X-Request-Id
Info endpoint
/autocurd-infoincludes:server.actualPort: the port in use when Autocrud created the serverserver.metricsPathFull: full metrics path including base path- config summary, schemas/joins, and last reload info
Utility endpoints (toggle in server config)
GET /autocurd-health→ { status: ok|degraded, schema.lastError }GET /autocurd-info→ version, config summary, schemas/joins, watch.dirs, watch.lastReloadAtGET /autocurd-list→ lists REST/GraphQL endpoints with sample curlGET /autocurd-openapi.json→ OpenAPI 3.0 for RESTGET /autocurd-sdl→ GraphQL SDL
Hot reload (default on)
- Watches schema directories; on add/change:
- Discovers new
*.jsonschemas withname - Reloads normalized schemas, rebuilds functions, REST/joins routers, and GraphQL SDL/server
- Surfaces last error in
/autocurd-health; last reload time in/autocurd-info
- Discovers new
- Disable via
server.schemaHotReloadEnabled: false
Config reference (summary)
server:{ port, portFallback, maxPortRetries, existingApp, basePath, graphqlPath, restEnabled, graphqlEnabled, loggingEnabled, logLevel, tracingEnabled, metricsEnabled, metricsPath, healthEnabled, infoEnabled, listEnabled, schemaHotReloadEnabled }- Metrics are exposed at
<basePath><metricsPath>; root/autocurd-metricsremains for compatibility.
- Metrics are exposed at
database:{ type: "file"|"sqlite"|"postgres"|"mongodb", url }schemas[name]:{ file, transform?, ops? }—ops:{ create?, read?, readOne?, update?, delete? }joins[name]:{ base, relations: [{ schema, localField, foreignField, as, type? }] }cache:{ enabled?, ttl? }functional:{ enabled? }
Tips & limits
- File adapter: single-process only; use SQLite/Postgres/Mongo for multi-process deployments or heavy writes.
- Tests: Postgres/Mongo integration are opt-in via
PG_URL/MONGO_URLenv vars. - Windows + better-sqlite3: requires native build tools; if not installed, SQLite stays optional and tests skip.
Testing & CI/CD
- Run tests locally:
npm test - Typecheck:
npm run typecheck - Formatting check:
npm run format:check - GitHub Actions CI:
.github/workflows/ci.yml- Runs typecheck, tests, build on push/PR (Node 18/20)
- Publishes to npm on tag
v*ifNPM_TOKENis set in repo secrets
Roadmap (advanced)
- Migrations and schema evolution
- Redis cache provider
- WebSockets for live updates
- Auth/RBAC and per-field permissions
- Cursor pagination across REST/GraphQL/joins
