Ecosystem

Katax Docs Hub

Katax

Katax Ecosystem

Three TypeScript packages to build, validate, and deploy Node.js APIs on VPS infrastructure with PM2.

katax-core 1.6.5 katax-service-manager 0.6.1 katax-cli 1.6.2

● Installation

npm install katax-core
npm install katax-service-manager
npm install -g katax-cli

# optional peer deps for katax-service-manager (install only what you use;
# each one is loaded lazily the first time you use that feature)
npm install pg mysql2 mongodb redis socket.io dotenv node-cron pino-pretty

● katax-core

katax-core is the validation layer. The entry point is k. Supports 20+ schema types, coercion, preprocess, async validators, composition and error utilities.

Quick Start

import { k, type kataxInfer } from 'katax-core';

const UserSchema = k.object({
  email: k.email(),
  name: k.string().minLength(2).maxLength(100),
  age: k.number().min(18).optional(),
});

type User = kataxInfer<typeof UserSchema>;

const result = UserSchema.safeParse(req.body);
if (!result.success) return res.status(400).json({ errors: result.issues });

// Inferred: { email: string; name: string; age: number | undefined }

Core Types

type SafeParseResult<T> = 
  | { success: true; data: T }
  | { success: false; issues: Issue[] }

interface Issue {
  path: (string | number)[];
  message: string;
}

interface ValidationResult {
  valid: boolean;
  issues: Issue[];
}

Schema API

All schemas implement: parse(input): T · safeParse(input): SafeParseResult<T> · validate(input): ValidationResult · parseAsync(input): Promise<T> · safeParseAsync(input): Promise<AsyncSafeParseResult<T>> · isValidAsync(input): Promise<AsyncValidationResult> · hasAsyncValidation(): boolean · describe(text) · toJsonSchema() · kataxInfer: T

All k factories

k.string()    k.number()    k.boolean()    k.bigint()   k.date()    k.twoDates()
k.object()    k.array()     k.tuple()      k.record()   k.set()     k.map()
k.union()     k.intersection()             k.discriminatedUnion()   k.lazy()
k.email()     k.file()      k.base64()     k.promise()
k.custom()    k.literal()   k.enum()
k.any()       k.unknown()   k.never()
k.nan()       k.null()      k.undefined()  k.void()
k.coerce      k.preprocess()

Modifiers (chainable, all schemas)

schema.optional()             // T | undefined
schema.nullable()             // T | null
schema.nullish()              // T | null | undefined
schema.default(value)         // Returns default if undefined
schema.transform(fn)          // Transform output type
schema.catch(value)           // Return fallback on validation failure
schema.refine(fn, msg?)       // Extra sync check
schema.asyncRefine(fn)        // Extra async check (DB, APIs...)
schema.brand('UserId')        // Nominal type
schema.describe('text')       // Description for JSON Schema / OpenAPI

optional vs nullable vs nullish

undefined (missing key) and null (explicitly empty) are different values. .optional() does not accept null; use .nullish() for both.

Modifierundefined / missing keynull
.optional()YesNo
.nullable()NoYes
.nullish()YesYes
.default(v)Yes (replaced by v)No

● String — k.string()

MethodDescription
minLength(n, msg?)Minimum length
maxLength(n, msg?)Maximum length
length(n, msg?)Exact length
email(msg?)Valid email format
url(msg?)Valid URL
uuid(msg?)Valid UUID (RFC 4122)
ip(msg?)Valid IPv4
regex(pattern, msg?)Regex match
datetime(msg?)Valid ISO 8601 datetime
jwt(msg?)Valid JWT format
cuid2(msg?)Valid CUID2
ulid(msg?)Valid ULID
emoji(msg?)Valid emoji sequence
cidr(msg?)Valid CIDR notation
startsWith(prefix, msg?)Must start with string
endsWith(suffix, msg?)Must end with string
includes(substr, msg?)Must include substring
oneOf(arr, msg?)Must be one of the options
notOneOf(arr, msg?)Must not be any of the options
lowercase(msg?)Lowercase only
uppercase(msg?)Uppercase only
alpha(msg?)Letters only [A-Za-z]
alphanumeric(msg?)Letters and numbers
ascii(msg?)ASCII characters only
noWhitespace(msg?)No spaces
nonempty(msg?) / notEmpty(msg?)Cannot be empty
trim()No leading/trailing spaces (no msg param)

● Number — k.number()

MethodDescription
min(value, msg?)Minimum value (>=)
max(value, msg?)Maximum value (<=)
equals(exact, msg?)Exact value
positive(msg?)Must be > 0
negative(msg?)Must be < 0
nonnegative(msg?)Must be >= 0
nonpositive(msg?)Must be <= 0
integer(msg?)Must be integer
finite(msg?)Must be finite
multipleOf(factor, msg?)Multiple of
between(min, max, msg?)Inclusive range
greaterThan(n, msg?)Strictly greater than
lessThan(n, msg?)Strictly less than
notEqual(n, msg?)Not equal to value
oneOf(arr, msg?)Must be one option
notOneOf(arr, msg?)Must not be any option

● Boolean — k.boolean()

MethodDescription
isTrue(msg?)Must be true
isFalse(msg?)Must be false
equals(val, msg?)Must equal the value

● Object — k.object(shape)

By default, extra keys are stripped. Use passthrough() to keep them or strict() to reject them.

MethodDescription
extend(extension)Add or override fields
merge(other)Merge two object schemas
pick([keys])Select specific fields
omit([keys])Exclude specific fields
partial()All fields optional
strict()Reject extra keys
passthrough()Allow extra keys in output
strip()Remove extra keys (default)
getShape()Get raw shape object
caseInsensitive()Case-insensitive key matching
const schema = k.object({
  name: k.string().minLength(2),
  age: k.number().min(18).optional(),
}).strict();

● Array — k.array(elementSchema?)

MethodDescription
minLength(n, msg?) / min(n, msg?)Minimum length
maxLength(n, msg?) / max(n, msg?)Maximum length
length(n, msg?)Exact length
notEmpty(msg?) / nonempty(msg?)Cannot be empty
unique(msg?)Unique elements (deep equality)
contains(element, msg?)Must contain element

● Email — k.email()

RFC 5322 compliant validation.

MethodDescription
domain(domain, msg?)Restrict to specific domain
domains([domains], msg?)Multiple allowed domains
domainPattern(pattern, msg?)Domain regex pattern
notDomains([blacklist], msg?)Domain blacklist
localMinLength(n, msg?)Min local part length
localMaxLength(n, msg?)Max local part length
localPattern(regex, msg?)Local part pattern
corporate(msg?)Block free providers
noPlus(msg?)Forbid '+' addressing
noDots(msg?)Forbid '.' in local part

● Date & TwoDates

k.date()

Input: a Date or an ISO 8601 string ('2024-01-15', '2024-01-15T10:30:00Z'). Output: Date object. Uses date-fns.

MethodDescription
min(dateStr, msg?)On or after the date
max(dateStr, msg?)On or before the date
between(start, end, msg?)Inclusive range
isFuture(msg?)Must be in the future
isPast(msg?)Must be in the past
format(formatStr, msg?)Must match format
isDateOnly(msg?)Date only (YYYY-MM-DD)
hasTime(msg?)Must include time
formatOutput(format)Output as formatted string

k.twoDates(separator?)

Input: Two ISO dates separated by separator (default '|'). Output: [Date, Date].

MethodDescription
maxDifference(days, msg?)Max days apart
minDifference(days, msg?)Min days apart
maxDifferenceHours(hours, msg?)Max hours apart
minDifferenceHours(hours, msg?)Min hours apart
order(ascending?, msg?)Chronological order (default asc)

● File — k.file()

Compatible with Browser File API and Node.js Multer objects.

MethodDescription
maxSize(bytes, msg?)Maximum size
minSize(bytes, msg?)Minimum size
type(mimeType, msg?)Exact MIME type
types([mimeTypes], msg?)Multiple MIME types
typePattern(pattern, msg?)MIME pattern (image/*)
extension(ext, msg?)File extension
extensions([exts], msg?)Multiple extensions
namePattern(regex, msg?)Filename regex
image(msg?)Shortcut: image/*
video(msg?)Shortcut: video/*
audio(msg?)Shortcut: audio/*
document(msg?)Shortcut: PDF, Word, Excel, plaintext

● Base64 — k.base64()

Validates base64-encoded strings, including data URLs.

MethodDescription
minDecodedSize(bytes, msg?)Min decoded size
maxDecodedSize(bytes, msg?)Max decoded size
mimeType(type, msg?)Exact data URL MIME type
mimeTypePattern(pattern, msg?)MIME pattern
image(msg?)Shortcut: image/*
pdf(msg?)Shortcut: application/pdf
json(msg?)Decoded must be valid JSON
dataUrl(msg?)Must be data URL

● Union / Intersection / Discriminated Union / Lazy

// Union — first schema that matches wins
k.union([k.string(), k.number()])

// Discriminated union — O(1) dispatch by a key; map keys are the discriminator values
k.discriminatedUnion('type', {
  cat: k.object({ type: k.literal('cat'), meow: k.boolean() }),
  dog: k.object({ type: k.literal('dog'), bark: k.boolean() }),
})

// Intersection — ALL schemas must match (merge objects)
k.intersection([
  k.object({ id: k.number() }),
  k.object({ name: k.string() })
])

// Lazy — deferred resolution for recursive types.
// The optional name becomes the JSON Schema $anchor of the recursion.
const categorySchema = k.lazy(() =>
  k.object({
    name: k.string(),
    subcategories: k.array(categorySchema)
  }),
  'Category'
);

● BigInt / Set / Map / Promise / Primitives

k.bigint()                 // min, max, positive, negative, nonnegative, nonpositive,
                           // between, greaterThan, lessThan, multipleOf, equals,
                           // notEqual, oneOf, notOneOf

k.set(k.number())          // Set<number>        .minSize(n) .maxSize(n) .nonempty()
k.map(k.string(), k.number()) // Map<string, number> .minSize(n) .maxSize(n) .nonempty()
k.promise(k.string())      // input must be a Promise; the resolved value is validated

k.nan()        // only NaN
k.null()       // only null
k.undefined()  // only undefined (inside an object: key must be absent)
k.void()       // null or undefined

● Literal / Enum / Tuple / Record / Any / Unknown / Never

k.literal('active')               // Exact value (===)
k.enum(['active', 'inactive'])    // String union
k.tuple([k.number(), k.string()]) // Fixed-length typed array
k.record(k.number())              // Record<string, number>
k.any()                           // Accepts everything
k.unknown()                       // Accepts everything (forces narrowing)
k.never()                         // Never matches

● Custom — k.custom<T>(validator)

const positiveEven = k.custom<number>((value, path) => {
  if (typeof value !== 'number')
    return [{ path, message: 'Must be number' }];
  if (value <= 0 || value % 2 !== 0)
    return [{ path, message: 'Must be positive even' }];
  return value;
});

// Add refinement
positiveEven.refine((v) => v < 1000, 'Must be less than 1000');

● Modifiers / Coercion / Preprocess

Coercion — k.coerce

Auto-converts types before validating:

k.coerce.number()   // "42" → 42, true → 1, false → 0
k.coerce.boolean()  // "true"/"1"/"yes" → true, "false"/"0"/"no" → false
k.coerce.string()   // 42 → "42", true → "true", null → ""
k.coerce.date()     // ISO string → Date, timestamp → Date

// Every method of the target schema is available:
k.coerce.number().positive().integer()
k.coerce.boolean().isTrue()
k.coerce.string().minLength(3).email()
k.coerce.date().isFuture()

Preprocess — k.preprocess(fn, schema)

k.preprocess(
  (val) => typeof val === 'string' ? val.trim().toLowerCase() : val,
  k.string().email()
)

Error Utilities

import { createIssue, issues, mergeIssues, isIssueArray, describeReceived, KataxError } from 'katax-core';

createIssue(['field'], 'error message');   // Issue object
issues(['field'], 'error message');         // Issue[]
mergeIssues(arr1, arr2);                    // Merge issue arrays
isIssueArray(value);                        // Type guard
describeReceived(value);                    // 'null' | 'undefined' | 'array' | typeof value

// KataxError is thrown by parse()/parseAsync(); e.issues has every issue
throw new KataxError(result.issues);

// KataxAsyncValidationRequiredError: parse()/safeParse() on a schema with async validators

● Async Validation

const usernameSchema = k.string().minLength(3).asyncRefine(async (value, path) => {
  const exists = await usersRepo.existsByUsername(value);
  return exists ? [{ path, message: 'Already taken' }] : [];
});

const result = await usernameSchema.safeParseAsync(input);

// Also available:
await schema.parseAsync(input);      // Throws KataxError
await schema.isValidAsync(input);    // { valid, issues }

// parse()/safeParse() throw KataxAsyncValidationRequiredError on schemas with async
// validators. validateSchema() picks sync or async for you:
import { validateSchema } from 'katax-core';
const r = await validateSchema(usernameSchema, req.body.username);

● JSON Schema & OpenAPI — toJsonSchema()

Every schema generates JSON Schema Draft 2020-12 that matches what parse() accepts: chained rules, nullability and optional keys included. Use it in OpenAPI 3.1 documents (Swagger UI, Redoc). OpenAPI 3.0 uses an older JSON Schema subset and won't accept type arrays, const or prefixItems.

const schema = k.object({
  name: k.string().minLength(2).describe("User's full name"),
  age: k.number().integer().min(18),
  nickname: k.string().nullish(),
  role: k.enum(['admin', 'user']).default('user'),
});

schema.toJsonSchema();
// {
//   type: "object",
//   properties: {
//     name: { type: "string", minLength: 2, description: "User's full name" },
//     age: { type: "integer", minimum: 18 },
//     nickname: { type: ["string", "null"] },
//     role: { type: "string", enum: ["admin", "user"], default: "user" }
//   },
//   required: ["name", "age"]
// }
SchemaJSON Schema
.optional() / .default(v)Key not in required (default also adds default: v)
.nullable()type: ["T", "null"], or anyOf with null for enum/literal/union
.nullish()Like .nullable() and key not required
k.number().integer()type: "integer"
.positive() / .greaterThan(n)exclusiveMinimum
String rules (regex, startsWith...)pattern; several are combined with allOf
k.object().strict()additionalProperties: false
k.union() / k.intersection()anyOf / allOf
k.discriminatedUnion()oneOf; each branch pins the key with const
k.tuple()prefixItems + items: false
Recursive k.lazy(fn, 'Name')$anchor: "Name" and $ref: "#Name"

Runtime-only rules (not representable in JSON Schema): refine, asyncRefine, custom, transforms, date comparisons, email domain rules, file/base64 size and MIME checks, regex flags.

● katax-service-manager

Singleton container that manages config, logger, databases, WebSocket, cron, cache, registry, health, and graceful shutdown.

Core

  • Katax class — katax singleton
  • new Katax() isolated instances
  • Katax.getInstance() / await Katax.reset()

Services

  • ConfigService · LoggerService
  • DatabaseService · CacheService
  • RegistryService · RedisStreamBridgeService

WebSocket and cron are used through katax.socket() / katax.cron(); their classes aren't exported so socket.io and node-cron stay optional.

Errors

  • KataxServiceError (base)
  • KataxConfigError · KataxDatabaseError
  • KataxRedisError · KataxWebSocketError
  • KataxRegistryError · KataxNotInitializedError

Transports

  • RedisTransport · CallbackTransport
  • TelegramTransport

Helpers

  • registerVersionToRedis
  • startHeartbeat · registerProjectInRedis

● Initialization, Environment & Lifecycle

Init

import { katax } from 'katax-service-manager';

await katax.init({
  loadEnv: true,
  appName: 'my-api',
  logger: { level: 'info', prettyPrint: true, enableBroadcast: false },
  hooks: {
    beforeInit: () => {},
    afterInit: () => {},
    beforeShutdown: () => {},
    afterShutdown: () => {},
    onError: (context, error) => {},
  },
  registry: { url: 'https://dashboard.example.com/api/services' },
});

Environment Helpers

katax.env('PORT', '3000');        // string with default
katax.env('PORT', 3000);          // number (auto-cast)
katax.env('DEBUG', false);        // boolean (auto-cast)
katax.envRequired('JWT_SECRET');  // throws if missing

katax.isDev    katax.isProd    katax.isTest
katax.nodeEnv  katax.appName   katax.version
katax.isInitialized

Configuration Service

const port = katax.config.get('PORT', 3000);
const exists = katax.config.has('API_KEY');
katax.config.set('custom', value);
katax.config.getAll();  // available on ConfigService, not on the public IConfigService type

Overrides (testing)

import { Katax } from 'katax-service-manager';
beforeEach(async () => { await Katax.reset(); });

katax.overrideService('db:main', mockDb);
katax.overrideService('logger', mockLogger);
katax.clearOverride('db:main');   // Remove specific
katax.clearOverride();            // Remove all

Shutdown & Hooks

katax.onShutdown(async () => {
  await cleanupCustomResource();
});

await katax.shutdown();
// Closes all services in order: hooks, DB, WS, cron, registry, bridges
// SIGTERM and SIGINT are handled automatically

● Database Service

Supports PostgreSQL, MySQL, MongoDB, and Redis with typed connections and configurable pool.

Setup

// PostgreSQL / MySQL
const db = await katax.database({
  name: 'main', type: 'postgresql',
  connection: { host: 'localhost', port: 5432, database: 'myapp', user: 'admin', password: 'secret' },
  pool: { max: 10, min: 2 },
});

// MongoDB
const mongo = await katax.database({
  name: 'analytics', type: 'mongodb',
  connection: { host: 'localhost', port: 27017, database: 'analytics' },
});

// Redis
const redis = await katax.database({
  name: 'cache', type: 'redis',
  connection: { host: '127.0.0.1', port: 6379 },
});

Typed Access

katax.database() is typed by type: ISqlDatabase (postgresql/mysql), IMongoDatabase or IRedisDatabase (| null with required: false). katax.db(name) returns the generic IDatabaseService, so narrow it:

import { katax, DatabaseService, type ISqlDatabase, type IRedisDatabase } from 'katax-service-manager';

// Compile-time cast
const sql = katax.db('main') as ISqlDatabase;
const users = await sql.query<User[]>('SELECT * FROM users WHERE id = $1', [userId]);

// Or the DatabaseService helpers (also check the type at runtime)
const db = katax.db('main') as DatabaseService;
db.asSql()    // ISqlDatabase — query<T>(sql, params?), getClient(), close()
db.asMongo()  // IMongoDatabase — getClient(), close()
db.asRedis()  // IRedisDatabase — redis(...args), close()

const redis = katax.db('cache') as IRedisDatabase;
await redis.redis('SET', 'key', 'value', 'EX', '60');   // RESP2 replies

● Cache Service

High-level Redis wrapper with automatic JSON serialization.

const cache = katax.cache('cache'); // Redis connection name
MethodDescription
get<T>(key)Get value (auto-deserializes JSON)
set(key, value, ttl?)Store with optional TTL (seconds)
del(key)Delete single key
delMany(keys[])Delete multiple keys
exists(key)Check if key exists
ttl(key)Remaining TTL
expire(key, seconds)Set expiration on existing key
incr(key)Increment by 1
incrBy(key, n)Increment by n
decr(key)Decrement by 1
mget<T>(keys[])Get multiple values
mset(entries[])Store multiple [key, value] pairs
clear(pattern?)Delete by pattern
stats()Redis INFO statistics
await cache.set('user:123', userData, 3600);
const user = await cache.get<User>('user:123');
await cache.del('user:123');
await cache.incr('page:views');
await cache.mset([['k1', v1], ['k2', v2]]);

● Logger & Transports

Pino-based structured logging with WebSocket broadcast and transport system.

// All log levels
katax.logger.trace('trace message');
katax.logger.debug('debug message');
katax.logger.info({ message: 'Server started', port: 3000 });
katax.logger.warn({ message: 'High memory', percent: 85 });
katax.logger.error({ message: 'Query failed', error: err });
katax.logger.fatal('fatal error');

// Child logger with context
const log = katax.logger.child({ service: 'payments', userId: 123 });
log.info({ message: 'Payment processed' });

// Routing options go in the second argument (never sent to transports)
katax.logger.info({ message: 'Trade executed', amount: 100 }, { broadcast: true, room: 'admins' });
katax.logger.error('Payment failed', { persist: true, skipTelegram: true });

Transports

import { RedisTransport, TelegramTransport, CallbackTransport } from 'katax-service-manager';

// Redis transport
const redisTransport = new RedisTransport(katax.db('cache') as IRedisDatabase, {
  streamKey: 'katax:logs', name: 'redis-errors',
});
redisTransport.filter = (log) => log.level === 'error' || log.persist === true;
katax.logger.addTransport(redisTransport);

// Telegram
const telegram = new TelegramTransport({
  botToken: katax.envRequired('TELEGRAM_BOT_TOKEN'),
  chatId: katax.envRequired('TELEGRAM_ALERTS_CHAT_ID'),
  levels: ['error', 'fatal'],
  includePersist: true, parseMode: 'Markdown',
});
katax.logger.addTransport(telegram);

// Management
katax.logger.removeTransport('telegram-errors');
await katax.logger.closeTransports();

● WebSocket (Socket.IO)

// Shared with Express (preferred)
const httpServer = createServer(app);
await katax.socket({
  name: 'main', httpServer,
  cors: { origin: '*' },
});

// Standalone with auth
await katax.socket({
  name: 'events', port: 3001,
  enableAuth: true,
  authValidator: async (token) => token === katax.envRequired('WS_SECRET'),
});

const ws = katax.ws('main');
MethodDescription
emit(event, data, room?)Emit event (optionally to room)
emitToRoom(room, event, data)Emit to specific room
on(event, handler)Listen for events
onConnection(handler)Handle new connections
hasRoomListeners(room)Has listeners in room?
getRoomClientsCount(room)Clients in room
hasConnectedClients()Any clients connected?
getConnectedClientsCount()Total connected clients
close()Close WebSocket server
getServer()Underlying Socket.IO Server (on the class, not on the IWebSocketService type: cast to use it)
ws.onConnection((socket) => {
  socket.on('message', (data) => { ... });
  socket.emit('welcome', { status: 'connected' });
  socket.join('room-123');
  socket.leave('room-123');
});

● Cron Service

Loads node-cron on first use. katax.cron() returns a Promise: await it to catch invalid schedules. katax.cronService is available after the first katax.cron().

await katax.cron({
  name: 'process-assets',
  schedule: '*/10 6-15 * * 1-5',
  task: processAssets,
  runOnInit: katax.isProd,
  timezone: 'America/Mexico_City',
  enabled: true,
});

// Advanced management
katax.cronService.getJobs();
await katax.cronService.startJob('process-assets');
katax.cronService.stopJob('process-assets');
katax.cronService.removeJob('process-assets');
katax.cronService.stopAll();

● Registry & Health Check

await katax.init({
  registry: {
    url: 'https://dashboard.example.com/api/services',
    apiKey: katax.env('REGISTRY_KEY'),
    heartbeatInterval: 30000,
    requestTimeoutMs: 5000,
    retryAttempts: 2,
    retryBaseDelayMs: 300,
    metadata: { env: katax.nodeEnv, region: 'us-east' },
  },
});

// Or custom handler
registry: {
  handler: {
    register: async (info) => { ... },
    heartbeat: async (info) => { ... },
    unregister: async (payload) => { ... },
  }
}

katax.isRegistered;         // boolean
katax.getServiceInfo();     // ServiceInfo | null

Health Check

const health = await katax.healthCheck();
// { status: 'healthy' | 'degraded' | 'unhealthy',
//   services: { databases: {}, sockets: {}, cron: boolean },
//   timestamp: number }

● Redis Stream Bridge & Heartbeat

Stream Bridge

const bridge = katax.bridge('cache', 'main', {
  appName: 'my-api',
  streamKey: 'katax:logs',
  group: 'katax-bridge-my-api',
  batchSize: 10,
  blockTimeout: 2000,
});
await bridge.start();
bridge.isRunning();
bridge.stop();

Heartbeat

// Managed (auto-cleanup on shutdown)
katax.heartbeat(
  { app: katax.appName, port: 3000, version: katax.version, intervalMs: 10000 },
  'cache', 'main',
);

// Manual helpers
import { registerProjectInRedis, startHeartbeat } from 'katax-service-manager';
await registerProjectInRedis(redisDb, { app: katax.appName, version: katax.version, port: PORT });
const hb = startHeartbeat(redisDb, { app: katax.appName, port: PORT, intervalMs: 10000 }, ws);
hb?.stop();

● Bootstrap Template

import { katax } from 'katax-service-manager';
import { createServer } from 'http';
import app from './app.js';

async function bootstrap(): Promise<void> {
  try {
    await katax.init({
      loadEnv: true,
      logger: { level: katax.env('LOG_LEVEL', 'info') as any, prettyPrint: katax.isDev, enableBroadcast: true },
    });

    await katax.database({
      name: 'main', type: 'postgresql',
      connection: {
        host: katax.envRequired('DB_HOST'),
        port: katax.env('DB_PORT', 5432),
        database: katax.envRequired('DB_NAME'),
        user: katax.envRequired('DB_USER'),
        password: katax.envRequired('DB_PASSWORD'),
      },
      pool: { max: 10, min: 2 },
    });

    await katax.database({
      name: 'cache', type: 'redis',
      connection: { host: katax.env('REDIS_HOST', '127.0.0.1'), port: 6379 },
    });

    const PORT = katax.env('PORT', '3000');
    const httpServer = createServer(app);
    await katax.socket({ name: 'main', httpServer, cors: { origin: '*' } });

    await katax.cron({ name: 'cleanup', schedule: '0 3 * * *', task: async () => { /* nightly */ } });

    httpServer.listen(PORT, () => {
      katax.logger.info({ message: 'Server running on http://localhost:' + PORT });
    });
    // SIGTERM/SIGINT are handled by katax (graceful shutdown)
  } catch (err) {
    console.error('Bootstrap failed:', err);
    process.exit(1);
  }
}

void bootstrap();

● katax-cli

CLI for scaffolding Express + TypeScript APIs, generating CRUD/endpoints/repositories, OpenAPI docs, and managing VPS deployments with PM2.

Scaffold init

Complete Express + TypeScript project with interactive prompts for DB, auth, validation, Swagger, katax-service-manager, WebSocket and Redis. Installs dependencies.

Generate endpoint & CRUD

Generates validators, controllers, handlers and routes (controllers start with sample data + TODOs). Nested resources supported. Auto-regenerates OpenAPI docs.

Deploy PM2

Full VPS deployment workflow: init (clone + PM2), update (pull + restart), rollback, logs, and status.

● Commands

katax init [project-name]

Scaffolds a complete Express + TypeScript + katax-core project.

FlagDescription
-f, --forceUse an existing directory (files are overwritten)
--pm <npm|pnpm>Default package manager in the prompt (default: pnpm)
--ignore-scriptsDefault answer: install with lifecycle scripts disabled
--write-npmrcDefault answer: write .npmrc with ignore-scripts=true

katax add endpoint <name>

Add a new endpoint with validator, controller, handler, and routes. HTTP methods, rate limiter and body fields are chosen in prompts; the route comes from the name (admin/users → /api/admin/users). The -m and -p flags are accepted but currently ignored.

katax generate crud <resource-name>

Aliases: gen, g. Generates CRUD endpoints: list, get by ID, create, update, delete.

FlagDescription
--no-authDefault "no" for the auth prompt. Auth uses authenticateToken from jwt.utils.ts (JWT projects only)

katax generate repository <name>

Writes src/api/<name>/<name>.repository.ts: a class over a katax.database() connection (PostgreSQL, MySQL or MongoDB) with findAll(), findById(), exists(), create(), update(), delete().

katax generate docs

Writes an OpenAPI 3.0 spec to src/openapi.json and Swagger UI at /docs. Run katax fix docs once so the build copies the spec to dist/ for production.

FlagDescription
-f, --forceForce regeneration
-o, --output <path>Output path (default: src/openapi.json)
-p, --port <port>Server port
-u, --url <url>Production server URL

katax info

Aliases: status, ls. Shows project info, katax package versions (and available updates), and routes.

Deploy

katax deploy init       # Initial PM2 setup (repo, branch, path, cluster, memory, env)
katax deploy update     # Pull, rebuild, restart
  -b, --branch <branch>
  --hard                # Hard reset
  -a, --app-name <name>
katax deploy rollback   # Revert commits
  -c, --commits <n>     # Default: 1
  -a, --app-name <name>
katax deploy logs       # View PM2 logs
  -l, --lines <n>
  -f, --follow
  -a, --app-name <name>
katax deploy status     # Show PM2 apps

# Default is cluster mode with max instances: pick 1 instance when using
# Socket.IO (needs sticky sessions) or the in-memory rate limiter.

Fix

katax fix docs        # Patch build script to copy openapi.json
  --skip-install
katax fix all         # Apply all fixes
  --skip-install
katax fix list        # List available patches

Global Options

--no-color     # Disable colored output
--verbose      # Enable verbose logging
-v, --version  # Show version

Examples

katax init my-api --pm pnpm --ignore-scripts --write-npmrc
katax add endpoint admin/audit/logs
katax generate crud admin/users --no-auth
katax generate repository products
katax generate docs -u https://api.example.com
katax deploy update -b production --hard

● Generators & Templates

Validator Generator

k.object() schemas per field, kataxInfer types and validate*() functions built on validateSchema().

Controller Generator

Business logic returning ControllerResult<T> via createResponse.success() / createResponse.error().

Handler Generator

Express middleware chaining validator + controller with sendResponse().

Route Generator

Express Router with method calls and JSDoc documentation.

Router Updater

Adds the import and router.use() to the main routes.ts. Prevents duplicates.

OpenAPI Generator

Scans validators and routes, generates OpenAPI 3.0 spec.

Shared Utilities

API Utils

  • ControllerResult<T> type (with pagination and cookies)
  • createResponse.success/error/validation/unauthorized/forbidden/custom
  • createSuccessResult() · createErrorResult()
  • validateSchema()
  • sendResponse()
  • setCookie() · clearCookie()

Stream Utils (SSE)

  • initSSE() · sendSSEEvent()
  • sendSSEComment() · closeSSE()
  • SSEStream class (send, sendError, close, isClosed)
  • sendChunked() · streamAsyncIterator() · streamArray()

Auth Utils

  • hashPassword() · verifyPassword() (bcryptjs)
  • hashPasswordArgon2() · verifyPasswordArgon2()
  • generateToken() · verifyToken() · decodeToken() · generateRefreshToken() (JWT)
  • generateRandomToken() · generateNumericCode()
  • sha256() · md5() · encrypt() · decrypt() · secureCompare()

JWT Middleware (jwt.utils.ts)

import { authenticateToken, requireRole, generateTokens } from '../shared/jwt.utils.js';

router.get('/admin', authenticateToken, requireRole('admin'), handler);
const { accessToken, refreshToken } = generateTokens({ userId: user.id, email: user.email });

● AI Skill — katax-ecosystem

The Copilot/AI skill for the Katax ecosystem. Knows versions, exports, patterns, CLI commands and troubleshooting.

What it covers

  • Versions and exports of each package
  • All schema types and their methods
  • All services and their API
  • All CLI commands with options
  • Complete bootstrap template
  • Troubleshooting and gotchas

When to invoke it

  • Create/extend APIs with Katax
  • Design validations with katax-core
  • Bootstrap with katax-service-manager
  • Generate endpoints/CRUD/repos with CLI
  • Configure DB, cache, cron, WebSocket
  • Deploy on VPS with PM2

● Claude Code Plugin

Everything below packaged as a Claude Code plugin: 5 skills covering the full ecosystem, plus a subagent that scaffolds new katax projects end to end (bootstrap, production hardening, optional MCP server). Install once, available in any repo.

Install

/plugin marketplace add LOPIN6FARRIER/katax-core-page
/plugin install katax-skills@katax-marketplace

If the install summary says Run /reload-plugins to activate., run that command.

Skills included

  • katax-core — validation schema API
  • katax-cli — scaffolding & generators
  • katax-service-manager — bootstrap & runtime
  • katax-ecosystem — cross-package guidance + production hardening (rate limiting, CORS, Helmet, cookies, sessions)
  • katax-mcp-server — MCP server + OAuth 2.1 for Claude connectors

Agent included

  • katax-project-builder — scaffolds a new production-ready katax API project: runs katax init, wires bootstrap, applies rate limiting/CORS/Helmet/cookies/sessions, and optionally adds a full MCP server with OAuth 2.1

● Current Published Status

  • katax-core 1.6.5: 30+ schema types, coercion, async validation, validateSchema(), file/base64, advanced email, union/intersection/discriminated union/lazy, preprocess, JSON Schema 2020-12 output, error utilities. Next release: toJsonSchema() fixes (nullable, integer, strict, discriminated, recursive $ref) and k.lazy(fn, name).
  • katax-service-manager 0.6.1: Singleton/instance, typed DB (PostgreSQL/MySQL/MongoDB/Redis, RESP2), cache, cron and WebSocket loaded lazily (truly optional peers), transports (Redis/Telegram/Callback), registry, health, lifecycle hooks, stream bridge, heartbeat, automatic SIGTERM/SIGINT shutdown.
  • katax-cli 1.6.2: Init with advanced prompts and pinned katax versions, update notices, CRUD/endpoint/repository generators, OpenAPI docs, PM2 deploy (init/update/rollback/logs/status), fixes, nested resources with safe path validation.