@webtypen/webframez-core
v0.3.39
Published
The core package of the typescript based webframez (web-framework) by webtypen.de
Readme
@webtypen/webframez-core
TypeScript-first backend framework core for Node.js.
This README reflects the current API in this repository and focuses on:
- Routing
- ApiScopes and ApiFunctions
- Datatables
- DataBuilder
- Console Commands
- Queue and Jobs
Requirements
- Node.js
>= 20(recommended) - TypeScript for development
Installation
npm i @webtypen/webframez-coreAgent-Regeln fuer Codex und Copilot
Das Paket enthaelt unter agents/webframez_rules.md empfohlene Arbeitsregeln fuer AI-Coding-Agents in Webframez-Projekten.
Nach der Installation liegt die Datei im Paket unter:
node_modules/@webtypen/webframez-core/agents/webframez_rules.mdDie Regeln beschreiben unter anderem:
- bevorzugte Nutzung von Model.objectId(...) statt eigener ObjectId-Resolver
- bevorzugte Nutzung der vorhandenen Helper wie StringFunctions, NumericFunctions und DateFunctions
- Verwendung der Webframez-Abstraktionen fuer Request, Response, Middleware, Storage, Datatables und Routes
Wichtig: AGENTS.md-Dateien haben keine universell standardisierte Import-Funktion. Die praktikable Variante ist deshalb, die Regeldatei aus dem Paket in der AGENTS.md des eigenen Projekts explizit zu referenzieren und projektspezifische Regeln lokal zu ergaenzen.
Beispiel fuer eine eigene AGENTS.md im Consumer-Projekt:
# AGENTS.md
Dieses Projekt verwendet @webtypen/webframez-core.
Uebernimm fuer alle Webframez-bezogenen Implementierungen die Konventionen aus:
./node_modules/@webtypen/webframez-core/agents/webframez_rules.md
Insbesondere gilt:
- fuer ObjectId-Konvertierungen Model.objectId(...) oder die passende Model-Instanzmethode verwenden
- vorhandene Webframez-Helper und Facades bevorzugen statt neue Utilities oder Resolver anzulegen
- fuer HTTP-, Routing-, Middleware-, Storage- und Datatable-Code die Webframez-APIs und Konventionen beibehalten
Projektspezifische Ergaenzung:
- dieses Projekt verwendet fuer Admin-Routen zusaetzlich die Middleware "admin-auth"
- Antworten im Backoffice sollen das Format { status, message, data } einhaltenWenn eine Regel aus der lokalen Projekt-AGENTS.md einer Regel aus der Paketdatei widerspricht, sollte die lokale Projektregel Vorrang haben.
Basic Web Setup
import { BaseKernelWeb, Request, Response, Route, WebApplication } from "@webtypen/webframez-core";
class Kernel extends BaseKernelWeb {
static controller = {
TestController: class {
async index(req: Request, res: Response) {
return res.send({ status: "ok" });
}
}
};
static middleware = {
auth: async (next: Function, reject: Function, req: Request, res: Response) => {
const isAllowed = true;
if (!isAllowed) {
return reject(new Error("Unauthorized"));
}
next(true);
}
};
}
const app = new WebApplication();
app.boot({
kernel: Kernel,
port: 3000,
basename: null,
routesFunction: () => {
Route.get("/", "TestController@index");
}
});WebApplication.boot(...) supports:
kernelportbasenameroutesFunctionmodulesconfigdatatablesapiScopeson the kernel or module providersjobsmodeonBoot
Routing
Register Routes
Route.get("/", "HomeController@index");
Route.post("/login", "AuthController@login");
Route.put("/users/:id", "UserController@update");
Route.delete("/users/:id", "UserController@delete");
Route.patch("/users/:id", "UserController@patch");You can also register inline handlers:
Route.get("/health", (req: Request, res: Response) => {
return res.send({ status: "ok" });
});Path Parameters and Wildcards
Supported path patterns:
- Required parameter:
/:id - Optional parameter:
/:id? - Single wildcard:
/* - Catch-all wildcard:
/**
Matched values are available in req.params.
Route Groups
Route.group({ prefix: "/admin", middleware: ["auth"] }, () => {
Route.get("/dashboard", "AdminController@dashboard");
Route.group({ prefix: "/users" }, () => {
Route.get("/:id", "AdminUserController@details");
});
});Route Middleware
Attach middleware by key via route options:
Route.get("/me", "AccountController@me", { middleware: ["auth"] });Route Domain Filter
You can restrict routes (or groups) to specific domains:
Route.group({ domains: ["websites.simplebis.com", "*.local.dev"] }, () => {
Route.get("/", "HomeController@index");
});
Route.get("/api/health", "HealthController@index", {
domains: ["api.example.com"]
});Domain matching also works behind reverse proxies (x-forwarded-host is respected).
If a wildcard domain matches, the extracted wildcard value is available in
req.routeDomainWildcard. The matched request hostname is available in
req.routeDomainMatch.
Middleware signature:
(next, reject, req, res) => {
// allow:
next(true);
// abort with error:
// reject(new Error("Forbidden"));
}Extend the Route Facade
You can add custom route registration helpers:
Route.extend("jsonGet", (route) => {
return (path: string, component: any, options?: any) => {
route.get(path, async (req: Request, res: Response) => {
res.header("Content-Type", "application/json");
return component(req, res);
}, options);
};
});
(Route as any).jsonGet("/x", (req: Request, res: Response) => res.send({ ok: true }));Request and Response
Request
Request includes:
method,url,headers,rawHeadersbody,bodyPlainquery,queryRawparamsrouteDomainMatch,routeDomainWildcardfilesmessage(nativeIncomingMessage)
Response
Common helpers:
res.status(201);
res.header("X-Test", "1");
res.send({ status: "success" });Also available:
sendCsv(...)download(filepath, options?)stream(req, filepath, filename, mimeType)registerEvent("after", fn)
ApiScopes and ApiFunctions
ApiScopes group callable ApiFunctions and can automatically expose them as normal HTTP routes. MCP exposure is intentionally not implemented in core; install @webtypen/webframez-ai and use APIFunctionsMCPServer when ApiFunctions should be exposed as MCP tools.
Define ApiFunctions
import { ApiFunction, ApiFunctionRequest, ApiFunctionResponse } from "@webtypen/webframez-core";
class CurrentUserFunction extends ApiFunction {
key = "current-user";
description = "Returns the current user.";
requestMethod = "GET";
params = {
includePermissions: { type: "boolean", default: false }
};
async handle(apiRequest: ApiFunctionRequest) {
return new ApiFunctionResponse({
user: apiRequest.context.user,
includePermissions: apiRequest.params.includePermissions
});
}
}requestMethod may be "GET", "POST", "PUT", "PATCH", "DELETE" or null.
Functions with requestMethod = null are not registered as normal HTTP routes, but they can still be used by higher-level integrations such as APIFunctionsMCPServer in @webtypen/webframez-ai.
Define ApiScopes
import { ApiScope, Request, Response } from "@webtypen/webframez-core";
class BackofficeApiScope extends ApiScope {
key = "backoffice";
apiBasePath = "/api/backoffice";
functions = [CurrentUserFunction];
async middleware(req: Request, _res: Response, abort: (message?: any, status?: number) => never) {
if (!req.headers.authorization) {
return abort("Unauthorized", 401);
}
return {
user: { id: "..." }
};
}
}For every function with a non-null request method, core registers:
apiBasePath + "/" + function.keyExample: GET /api/backoffice/current-user.
The scope middleware runs before the function. Its return value is passed to the function as apiRequest.context. Calling abort(message, status) stops execution and returns an error response with that status.
Register ApiScopes
Register scopes on the web kernel:
import { BaseKernelWeb } from "@webtypen/webframez-core";
class Kernel extends BaseKernelWeb {
static apiScopes = [BackofficeApiScope];
}Modules can register scopes as instance properties:
import { ModuleProvider } from "@webtypen/webframez-core";
class BackofficeModuleProvider extends ModuleProvider {
apiScopes = [BackofficeApiScope];
}Params and Validation
GET functions validate params from req.query; all other request methods validate params from req.body.
Supported descriptor types include:
stringnumber/floatinteger/intboolean/boolObjectIdoptionarrayobject
Defaults are applied before required checks. Unknown types are passed through unchanged but still respect required and default.
Datatables
Use Datatable + DatatableRegistry + DatatableController.
Define a Datatable
import { Datatable, Request } from "@webtypen/webframez-core";
export class UsersTable extends Datatable {
collection = "users";
perPage = 25;
columns = {
name: { label: "Name" },
email: { label: "E-Mail" }
};
filter = {
name: { type: "text", mapping: "name" }
};
aggregation = async (req: Request) => {
return [{ $sort: { created_at: -1 } }];
};
}Register Datatables
app.boot({
// ...
datatables: {
users: UsersTable
}
});Datatable Endpoints
Use DatatableController in your routes:
import { DatatableController, Route } from "@webtypen/webframez-core";
Route.post("/api/datatable", "DatatableController@restApi");
Route.post("/api/datatable/export", "DatatableController@tableExport");restApi supports:
- init requests (
init_request) - paginated data
- selectable functions (
apiFunction)
tableExport uses your table exports definition.
DataBuilder
Use DataBuilder + DataBuilderController for schema-driven CRUD flows.
Define and Register Types
import { DataBuilder } from "@webtypen/webframez-core";
const builder = new DataBuilder();
builder.registerType({
key: "user",
singular: "User",
plural: "Users",
schema: {
version: "1.0.0",
collection: "users",
fields: {
email: { type: "text", required: true, unique: { match: {} } },
age: { type: "integer" }
}
}
});You can also use:
registerModelType(...)registerFieldType(...)
Expose the REST API
import { DataBuilderController, Route } from "@webtypen/webframez-core";
class MyDataBuilderController extends DataBuilderController {
constructor() {
super(builder);
}
}
// Register controller in kernel, then:
Route.post("/api/builder", "MyDataBuilderController@restApi");__builder_rest_api actions:
typedetailsdetails-newdatasavedeleteapi-autocomplete
Console Commands
Use ConsoleApplication with BaseKernelConsole.
import { BaseKernelConsole, ConsoleApplication, ConsoleCommand } from "@webtypen/webframez-core";
class HelloCommand extends ConsoleCommand {
static signature = "hello";
static description = "Print a greeting";
async handle() {
this.success("Hello world");
}
}
class KernelConsole extends BaseKernelConsole {
static commands = [HelloCommand];
}
const app = new ConsoleApplication();
app.boot({
kernel: KernelConsole,
config: {}
});ConsoleCommand helpers include:
getArguments(),getOptions(),getOption(...)ask(...)write,writeln,info,warning,success,error- progress helpers (
progress,progressIncrement,progressFinish)
Queue and Jobs
Queue workers run through console commands and use queue_jobs storage.
Create a Job
import { BaseQueueJob } from "@webtypen/webframez-core";
export class SendMailJob extends BaseQueueJob {
async handle(job: any) {
this.log("Sending mail", job.payload);
// do work...
// optional delayed re-run:
// return this.executeAgain(5, "minutes");
}
}Create a new queue entry:
await SendMailJob.create({
payload: { userId: "..." },
priority: 5,
worker: null
});Register Jobs
app.boot({
// ConsoleApplication or WebApplication
jobs: [SendMailJob]
});Queue Config
Configure workers in config.queue:
export default {
workers: {
default: {
is_active: true,
jobclasses: ["SendMailJob"],
automation: [
{
jobclass: "SendMailJob",
executions: [
["every_x_mins", 15],
["daily", "08:00"],
["every_hour", 5],
["mondays", "09:30"]
]
}
]
}
}
};Built-in Queue Commands
queue:startqueue:statusqueue:stopqueue:logqueue:workerqueue:worker:autorestart
Additional built-in command:
build
Backups
Backups are configured through config.backup, usually by importing config/backup.ts next to your database and queue configs. Core ships the local filesystem output driver and a generic adapter registry.
export default {
defaults: {
workDir: "storage/backups/.work",
outputDir: "storage/backups",
filename: "{key}_{date}_{time}",
zip: true,
cleanupWorkDir: true,
retention: {
keepLast: 10,
maxAgeDays: 30,
runAfterBackup: true
}
},
types: {
daily_full: {
is_active: true,
files: [
{
from: "storage",
to: "storage",
include: ["**/*"],
exclude: ["queue/**", "backups/**"]
}
],
databases: [
{
connection: "default",
to: "database/default",
options: {}
}
],
outputs: [
{
driver: "local",
path: "storage/backups/daily",
retention: {
keepLast: 14,
maxAgeDays: 60,
runAfterBackup: true
}
}
],
automation: {
worker: "worker_1",
executions: [["daily", "02:00"]],
priority: 10
}
}
}
};Built-in backup commands:
backup:listbackup:run <key> [--dry-run] [--channel=local]backup:queue <key> [--worker=worker_1] [--priority=10]backup:cleanup <key> [--channel=local] [--dry-run]
Remote outputs are optional packages and must be registered by the application before the backup config is used:
import { BackupOutputDrivers } from "@webtypen/webframez-core";
import { GoogleDriveBackupOutputDriver } from "@webtypen/webframez-backups-googledrive";
import { OneDriveBackupOutputDriver } from "@webtypen/webframez-backups-onedrive";
import { SftpBackupOutputDriver } from "@webtypen/webframez-backups-sftp";
import { FtpBackupOutputDriver } from "@webtypen/webframez-backups-ftp";
BackupOutputDrivers.register("google_drive", GoogleDriveBackupOutputDriver);
BackupOutputDrivers.register("one_drive", OneDriveBackupOutputDriver);
BackupOutputDrivers.register("sftp", SftpBackupOutputDriver);
BackupOutputDrivers.register("ftp", FtpBackupOutputDriver);
export default {
types: {
daily_google_drive: {
is_active: true,
files: [{ from: "storage", to: "storage", exclude: ["backups/**", "queue/**"] }],
outputs: [
{
driver: "google_drive",
folderId: process.env.GOOGLE_DRIVE_BACKUP_FOLDER_ID,
auth: {
keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS
},
retention: {
keepLast: 30,
maxAgeDays: 90,
runAfterBackup: true
}
}
],
automation: {
worker: "worker_1",
executions: [["daily", "03:00"], ["daily", "12:00"]]
}
}
}
};Available adapter packages:
@webtypen/webframez-backups-googledrive@webtypen/webframez-backups-onedrive@webtypen/webframez-backups-sftp@webtypen/webframez-backups-ftp
Database backups are delegated to the configured database driver via backup(client, options). Implement that hook in your driver to write dump files into options.targetDir.
Modules
You can load module providers via modules in WebApplication.boot(...).
A module provider can:
- register controllers/middleware
- define
boot() - define
routes()
Lambda Mode
LambdaApplication uses the same router in AWS Lambda mode:
import { LambdaApplication } from "@webtypen/webframez-core";
const app = new LambdaApplication();
export const handler = (event: any, context: any) => {
return app.boot(event, context, {
kernel: Kernel,
routesFunction: () => {
Route.get("/status", (req, res) => res.send({ ok: true }));
}
});
};