@json-express/core
v2.0.0
Published
The Microkernel and IoC container for JSON Express
Maintainers
Readme
@json-express/core
The IoC Kernel and the Auto-Discovery Engine for JSONExpress. Provides the json-express binary that runs your server.
This package contains zero HTTP logic, zero database logic, and zero CLI prompts. It plays two roles:
- The Kernel — an
awilix-backed IoC container that defines the framework's interface contracts and orchestrates the boot lifecycle (register → onRegister → generate routes → boot transport → onReady). - The Auto-Discovery Engine — at startup, it reads the user's local
package.json, discovers every installed@json-express/*plugin, and wires them into the kernel automatically.
📦 Installation
For the expert path, install core alongside the specific plugins you want — at minimum a config provider, a database adapter, an API generator, a transport, and a logger:
npm install @json-express/core @json-express/config-env \
@json-express/adapter-memory @json-express/api-rest \
@json-express/transport-express @json-express/logger-consoleFor the beginner path, just install @json-express/boot — it brings core plus the recommended default stack via npm dependency resolution.
Run the server with the bundled binary:
npx json-express🧠 The Auto-Discovery Engine
When json-express starts, the engine boots in phases:
- Discover the config module first. It scans
package.jsonfor@json-express/config-*packages and instantiates the config provider before anything else, sojex.*orJEX.*env values can drive plugin selection in subsequent steps. At least oneconfig-*plugin is required — without it,coreexits with a clear error. - Discover the rest of the installed
@json-express/*plugins, bucketed by category prefix:transport-,adapter-,api-,logger-,docs-,middleware-,seeder-,id-,email-,kv-,queue-,plugin-. - Resolve one plugin per required category. Required categories are
config-,adapter-,api-,transport-,logger-. If a category has multiple installs and nojex.<category>orJEX.<CATEGORY>env override,coreerrors out and points the user atnpx jex configure. - Wire everything into the kernel and call
kernel.boot(...)— the API generator builds routes, the database loads data, and the transport starts listening.
The engine is non-interactive — for plugin disambiguation, see @json-express/cli's jex configure wizard.
🏗️ Schema Definition (Models)
JSONExpress is a schema-driven engine. You can define your data models in pure JSON or use the powerful TypeScript API via defineModel.
We support two API styles for TypeScript definitions, allowing you to choose between ergonomic Zod-style builders or a declarative Options-Object format.
1. The Fluent Zod-Style API (Recommended)
This provides chainable methods with perfect IDE autocomplete, ideal for TypeScript developers:
import { defineModel, types } from '@json-express/core';
export default defineModel({
name: 'product',
fields: {
name: types.string().required().unique(),
price: types.number().required().default(0).min(0),
isActive: types.boolean().default(true),
brandId: types.relation({ target: 'brand', type: 'many-to-one' }).onDelete('RESTRICT')
}
});2. The Options-Object API
This maps 1:1 with pure .json models, ideal for strict serialization:
import { defineModel, types } from '@json-express/core';
export default defineModel({
name: 'product',
fields: {
name: types.string({ required: true, unique: true }),
price: types.number({ required: true, default: 0, min: 0 }),
isActive: types.boolean({ default: true }),
brandId: types.relation({ target: 'brand', type: 'many-to-one', onDelete: 'RESTRICT' })
}
});Supported Properties
Every field type supports the following BaseOptions:
required?: booleandefault?: anyunique?: booleanindex?: boolean
Specific Types:
types.string()supportsmaxLength,minLength.types.number()supportsmin,max.types.relation({ target, type })requirestarget(collection name) andtype(one-to-many,many-to-one, etc.), and supportsforeignKey,onDelete.types.boolean(),types.date(),types.id()support allBaseOptions.
Per-Model Validation
Models may declare a validation block — read by @json-express/middleware-validation at boot, inert metadata when that package isn't installed.
import { defineModel, types } from '@json-express/core';
import { z } from 'zod';
export default defineModel({
fields: {
name: types.string({ required: true }),
price: types.number({ required: true, min: 0 }),
},
validation: {
create: { body: z.object({ name: z.string().min(2), price: z.number().positive() }) },
update: { body: (baseline) => baseline.partial() }, // builder form
list: { query: z.object({ limit: z.coerce.number().int().max(100).optional() }) },
},
});Each slot accepts a Validator (anything with safeParse), a ValidatorBuilder (baseline) => Validator over the auto-derived field baseline, or {} to opt into the baseline as-is. Core never imports Zod — the structural Validator interface lets a future middleware-validation-yup ship without forking.
Custom Endpoints
The endpoints block accepts both the bare-function form and an object form that lets validation sit next to the handler:
endpoints: {
'GET /:id/play': async (req, res, ctx) => { /* ... */ }, // sugar
'POST /search': { // object form
handler: async (req, res, ctx) => { /* ... */ },
validation: {
body: z.object({ q: z.string().min(2) }),
},
},
}Fieldless Route-Only Models — defineRoutes()
fields is optional. For non-entity routes (/search, /auth/login, webhooks), use defineRoutes() — sugar for defineModel({ exposeApi: false, ... }):
import { defineRoutes } from '@json-express/core';
import { z } from 'zod';
export default defineRoutes({
endpoints: {
'POST /login': {
handler: async (req, res, ctx) => { /* ... */ },
validation: { body: z.object({ email: z.string().email(), password: z.string().min(8) }) },
},
},
});The filename is the mount prefix (models/auth.ts → /auth/login). Fieldless models skip CRUD codegen, schema-derived OpenAPI components, and auto-seeding — they declare behavior only.
🧩 The Standardized Contracts
Any official or community plugin must implement one of the interfaces exported from @json-express/core/types:
IConfigProvider(Configuration Layer)IDatabaseAdapter(Storage Layer)IApiGenerator(Paradigm Layer)ITransport(Server Layer)ILogger(Observability Layer)IDocProvider(Documentation Layer)IMiddleware(Interceptor Layer) — implementations may opt intosetSchemas?(schemas)to receive the loaded model set once at boot. The runner calls it after middlewares are registered;middleware-validationuses this to compile its rule table.ISeeder(Data Seeding Layer)IPlugin(Lifecycle Layer)IIdGenerator(ID Layer)IEmailProvider/IKvStore/IQueueAdapter(Optional Side-Effect Layers)
Request Context & Tracing
The core package provides a robust mechanism for request-scoped correlation using Node.js AsyncLocalStorage:
RequestContext— a global utility to store and retrievetraceIdandstartTimeconsistently across the entire framework without parameter drilling. Logger plugins (e.g.@json-express/logger-console,@json-express/logger-pino) read this to enrich every log entry withtraceId.
Core Utilities
core also exposes Twelve-Factor configuration helpers used by other plugins:
buildNestedConfigFromEnv(envVars, namespace)— converts flatjex.*orJEX.*env vars into a nested config object.getNestedValue(obj, path, default)/setNestedValue(obj, path, value)— dot-notation access.deepMerge(...objects)— right-to-left precedence object merge.
🚀 Programmatic Usage
If you are building a custom Node.js script and want to bypass auto-discovery, you can instantiate the kernel and register your plugins manually. Note that registerLogger must be called before boot — the kernel no longer ships an internal fallback.
import { JsonExpressKernel } from '@json-express/core';
import { EnvConfigProvider } from '@json-express/config-env';
import { ConsoleLogger } from '@json-express/logger-console';
import { MemoryDatabaseAdapter } from '@json-express/adapter-memory';
import { RestApiGenerator } from '@json-express/api-rest';
import { ExpressTransport } from '@json-express/transport-express';
const kernel = new JsonExpressKernel();
const configProvider = new EnvConfigProvider(process.cwd());
const logger = new ConsoleLogger();
kernel.registerConfigProvider(configProvider);
kernel.registerLogger(logger);
kernel.registerDatabase(new MemoryDatabaseAdapter({ configProvider, logger }));
kernel.registerApiGenerator(new RestApiGenerator({ database: kernel.container.resolve('database'), configProvider, logger }));
kernel.registerTransport(new ExpressTransport({ configProvider, logger }));
await kernel.boot(['users', 'posts']);