TypeScript · Backend · MIT
IN DEVELOPMENT

Write the application.
Not the plumbing.

A TypeScript backend for people who build applications. Secure by default, small by design: routes, sessions, access rights, migrations, Postgres or SQLite, workers and logs in one package, written together.

$npm install @r-libre/z4js

Most Node backends start as a blank Express app and grow by accretion.

z4js starts with everything refused.

01 / MODEL

A small mental model.

The main concepts are deliberately few. Use TypeScript and Node when they already solve the problem, and add an abstraction only where it earns its place.

01

Config

What the deployment is. The class is the schema of the file: an unknown key is an error, and secrets live in separate files.

this.secret( "mail.passwordFile" )
02

Model

What the database must look like. It inspects the real state at every start and brings it where it must be.

hasTable · hasField
03

EndPoints

What a resource answers. An object that only notes its routes: created anywhere, tested alone, mounted in several groups.

this.get( "/item/:id", … )
04

RouteGroup

Who may reach it. A prefix and a decision, guarded or unprotected, written in the main.

RouteGroup.guarded( … )
05

Access

What a user may do. Four actions per resource, nothing else, checked in every handler.

notes/create
06

Sessions

Who the user is. Opaque tokens, no cookies, refresh tokens rotated on every use.

sessions.guard
07

Workers

What runs beside the requests. Classes registered by name, messages both ways, named mutexes.

workers.call( … )
08

Logger

What happened. One line per event: a stable name, then the data in JSON.

req.log.info( "note.created" )
02 / JUST TYPESCRIPT

The whole surface, in the main.

A group is either guarded or unprotected, and you have to say which one. An open route stands out in review instead of hiding behind a forgotten middleware.

  • Guarded or unprotected, always explicit
  • Plain classes, mounted in groups
  • No decorators, no injection container
  • Code that runs when you call it
main.tsTypeScript
const api = RouteGroup.guarded( "/api", sessions.guard )
  .add( "/notes", new NotesEP( deps ) )
  .add( "/live", live );

const auth = RouteGroup.unprotected( "/auth" )
  .add( "/", sessions.endPoints );

await serve( {
  config, logger,
  groups: [api, auth],
  statics: [{ path: "/", folder: config.www }]
} );
One main · every route visible · nothing implicit
03 / SECURITY

Secure by default.
Not by checklist.

Security is not a chapter at the end of the documentation. It is what happens when you write nothing special.

SAME RULES EVERYWHERE3

Three rules.

Anything not explicitly allowed is refused. The client gets a status and a short message, never an internal detail. Every value from a client is checked before you use it.

SessionsOpaque tokensRefresh token rotationTheft detection PBKDF2-SHA256Step-upAccess rightsRate limits CORSSecurity headersRequest idCentral error handler Security logWebSocket ticketsDeclared uploadsStrict configuration Mandatory TLSRefused by default
SESSIONSno cookies · no JWT

A stolen token ends the session.

Opaque tokens, only their SHA-256 hash stored. 15 minute access tokens, refresh tokens rotated on every use: a reused one ends the session. A sensitive route adds a step-up filter.

this.del( "/item/:id", this.on_delete,
  { filter: sessions.stepUp } );

// without a recent confirmation of identity:
// 403 step-up required
ACCESS RIGHTSresource/action

Four actions per resource. Nothing else.

An import creates, so it needs notes/create: no right is ever invented for a feature. In debug mode, a handler that forgets to check access is reported.

notes/createnotes/readnotes/updatenotes/deletenotes/**
ERRORS + LOGSnothing leaks

The client gets a status. The log gets the cause.

One central handler: the cause goes to the log, never to the response. Security events come from a closed list, never filtered, in their own file.

throw new HttpError( 404, "unknown note" );auth.login.failed · auth.unauthorized · auth.stepup.failed
BUILT FOR APPLICATIONS

The pieces a business backend actually needs.

One package provides them, written together, with the same rules everywhere.

ROUTES

EndPoints

A handler is a method. paramValue, bodyValue and queryValue read one named value, convert it, check it, and return the right TypeScript type. A missing, malformed, too long or out of range value is a 400 that names the parameter and never echoes its value.

async on_create( req: Request, res: Response ) {
  await this.need( req, "notes/create" );

  const title = this.bodyValue( req, "title", "string", { maxlength: 100, trim: true } );
  const text = this.bodyValue( req, "text", "string", { maxlength: 10_000 } );
  ...
  res.status( 201 ).json( { id } );
}
DATABASE

Model

No numbered migration files. A model inspects the real state of the database and brings it where it must be, because in development the database is often edited by hand. Every model migrates in one transaction: it all succeeds, or nothing changed.

override async onMigrate( db: Db, version: number ): Promise<number> {
  if( !await this.hasTable( db, "notes" ) ) {
    await db`create table notes ( ... )`;
  }

  if( !await this.hasField( db, "notes", "author" ) ) {
    await db`alter table notes add column author text`;
  }

  return Math.max( version, 2 );
}
UPLOADS

Files

Refused unless declared. Each file field has its size limit, files are streamed to disk under a random name, and whatever is not kept is deleted when the answer is sent.

400
413
415
THREADS

Workers

Classes registered by name in a single entry file. Messages go both ways, a worker processes them one at a time, and its log lines are written by the main thread.

await workers.start( "stats" );

const result = await workers.call(
  "stats", "count", { texts } );
WEBSOCKETS

Channel

Mounted in a group like end points. In a guarded group, a socket opens only with a one-time ticket, bound to the endpoint and the user, valid one second.

POST /api/live           → ticket
WS   /api/live?ticket=…  → socket
04 / SQL

Postgres or SQLite.
One style.

Postgres through postgres.js for sites, SQLite through node:sqlite for small projects and desktop apps. Both are written the same way, with tagged templates, and values are always bound. No ORM, no query builder.

const [note] = await sql`select * from notes where id = ${id}`;
await sql`insert into notes ${sql( note )}`;
await sql`update notes set ${sql( changes, "title", "text" )} where id = ${id}`;
Two engines. Same code. No added dependency for SQLite.
05 / DEPENDENCIES

Very few dependencies.

Five packages, that is all. No ORM, no validation library, no logger, no session store, no JWT library, no upload middleware. Sources are published as TypeScript, as is: what you debug is what was written.

TypeScript source5 dependenciesNo side effect at import
YOUR BACKENDdepends on
express 5postgresws@fastify/busboyx4buildORMJWTvalidation
↓ build
demo bundle1280 KB → 776 KB
Its workers bundle only what they use: 684 KB → 29 KB.
06 / DEVELOPER EXPERIENCE

Logs you can read.
No ceremony.

z4js keeps the daily work short: readable logs, an API description read from your sources, and threads you can actually debug.

Logs you can grepOne line per event: date, level, request id, event name, then the data in JSON.
z4js apidocThe TypeScript compiler reads your sources and writes an OpenAPI 3 description. No annotations, no decorators.
TypeScript end to endSources are published as TypeScript: what you debug is what was written.
Threads you can debugEach thread carries its name in the debugger, and calls never time out while you sit on a breakpoint.
Try the demo
$ git clone https://github.com/rlibre/z4js
$ cd z4js/demo/frontend
$ npm install && npm run build
$ cd ../backend
$ npm install && npm run build
$ npm run start:log

# a notes application: z4js backend, x4js frontend
07 / HUMANS + MACHINES

Readable by humans.
Predictable for machines.

z4js ships an AI guide (the rules) and an AI context (the API). Agents can also inspect the TypeScript sources shipped in node_modules/@r-libre/z4js.

SECURE BY DEFAULT. SMALL BY DESIGN.

Use the simplest tool
that fits.

CLIENT SIDE

x4js.
The same idea, client side.

x4js is the UI counterpart of z4js: the same philosophy on both sides. Plain TypeScript classes, very few dependencies, and code that runs when you call it. More than 50 components for dense, professional interfaces.

x4jsTypeScript UI framework · MITVisit x4js.org →