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.
| Modifier | undefined / missing key | null |
.optional() | Yes | No |
.nullable() | No | Yes |
.nullish() | Yes | Yes |
.default(v) | Yes (replaced by v) | No |
● String — k.string()
| Method | Description |
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()
| Method | Description |
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()
| Method | Description |
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.
| Method | Description |
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?)
| Method | Description |
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.
| Method | Description |
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.
| Method | Description |
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].
| Method | Description |
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.
| Method | Description |
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.
| Method | Description |
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
● 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"]
// }
| Schema | JSON 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
| Method | Description |
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');
| Method | Description |
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.
| Flag | Description |
-f, --force | Use an existing directory (files are overwritten) |
--pm <npm|pnpm> | Default package manager in the prompt (default: pnpm) |
--ignore-scripts | Default answer: install with lifecycle scripts disabled |
--write-npmrc | Default 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.
| Flag | Description |
--no-auth | Default "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.
| Flag | Description |
-f, --force | Force 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.