Core
Kataxclass —kataxsingletonnew Katax()isolated instancesKatax.getInstance()/Katax.reset()
Services
ConfigService·LoggerServiceDatabaseService·WebSocketServiceCronService·CacheServiceRegistryService·RedisStreamBridgeService
Three TypeScript packages to build, validate, and deploy Node.js APIs on VPS infrastructure with PM2.
npm install katax-core
npm install katax-service-manager
npm install -g katax-cli
# optional peer deps for katax-service-manager
npm install pg mysql2 mongodb redis socket.io dotenv node-cron pino-pretty katax-core is the validation layer. The entry point is k. Supports 20+ schema types, coercion, preprocess, async validators, composition and error utilities.
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 }); type SafeParseResult<T> =
| { success: true; data: T }
| { success: false; issues: Issue[] }
interface Issue {
path: (string | number)[];
message: string;
}
interface ValidationResult {
valid: boolean;
issues: Issue[];
} 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 · kataxInfer: T
k.string() k.number() k.boolean() k.date() k.twoDates()
k.object() k.array() k.tuple() k.record()
k.union() k.intersection() k.lazy()
k.email() k.file() k.base64()
k.custom() k.literal() k.enum()
k.any() k.unknown() k.never()
k.coerce k.preprocess() schema.optional() // T | undefined
schema.nullable() // T | null
schema.default(value) // Returns default if undefined
schema.transform(fn) // Transform output type
schema.catch(value) // Return fallback on validation failure 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 |
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?) | Cannot be empty |
trim() | No leading/trailing spaces (no msg param) |
k.number()| Method | Description |
|---|---|
min(value, msg?) | Minimum value (>=) |
max(value, msg?) | Maximum value (<=) |
length(exact, msg?) | Exact value |
positive(msg?) | Must be > 0 |
negative(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 |
nonempty(msg?) | Alias for min(1) |
k.boolean()| Method | Description |
|---|---|
isTrue(msg?) | Must be true |
isFalse(msg?) | Must be false |
equals(val, msg?) | Must equal the value |
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(); k.array(elementSchema?)| Method | Description |
|---|---|
minLength(n, msg?) | Minimum length |
maxLength(n, msg?) | Maximum length |
length(n, msg?) | Exact length |
notEmpty(msg?) | Cannot be empty |
unique(msg?) | Unique elements (deep equality) |
contains(element, msg?) | Must contain element |
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 |
k.date()Input: ISO 8601. 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) |
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 |
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 — at least one schema must match (short-circuit)
k.union([k.string(), k.number()])
// 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
const categorySchema = k.lazy(() =>
k.object({
name: k.string(),
subcategories: k.array(categorySchema)
})
); 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 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'); k.coerceAuto-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
// Proxy validation methods available:
k.coerce.number().positive().integer()
k.coerce.boolean().isTrue()
k.coerce.string().minLength(3).email()
k.coerce.date().isFuture() k.preprocess(fn, schema)k.preprocess(
(val) => typeof val === 'string' ? val.trim().toLowerCase() : val,
k.string().email()
) import { createIssue, issues, mergeIssues, isIssueArray, 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
// KataxError is thrown by parse()/parseAsync()
throw new KataxError(result.issues); 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 } Singleton container that manages config, logger, databases, WebSocket, cron, cache, registry, health, and graceful shutdown.
Katax class — katax singletonnew Katax() isolated instancesKatax.getInstance() / Katax.reset()ConfigService · LoggerServiceDatabaseService · WebSocketServiceCronService · CacheServiceRegistryService · RedisStreamBridgeServiceKataxServiceError (base)KataxConfigError · KataxDatabaseErrorKataxRedisError · KataxWebSocketErrorKataxRegistryError · KataxNotInitializedErrorRedisTransport · CallbackTransportTelegramTransportregisterVersionToRedisstartHeartbeat · registerProjectInRedisimport { 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' },
}); 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 import { Katax } from 'katax-service-manager';
beforeEach(() => Katax.reset());
katax.overrideService('db:main', mockDb);
katax.overrideService('logger', mockLogger);
katax.clearOverride('db:main'); // Remove specific
katax.clearOverride(); // Remove all 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 Supports PostgreSQL, MySQL, MongoDB, and Redis with typed connections and configurable pool.
// 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 },
}); const db = katax.db('main');
db.asSql() // ISqlDatabase — query<T>(sql, params?), getClient(), close()
db.asMongo() // IMongoDatabase — getClient(), close()
db.asRedis() // IRedisDatabase — redis(...args), close()
// SQL query
const users = await db.asSql().query<User[]>('SELECT * FROM users WHERE id = $1', [userId]);
// Redis command
await db.asRedis().redis('SET', 'key', 'value', 'EX', '60');
await db.asRedis().redis('GET', 'key'); 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]]); 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' });
// Broadcast to WebSocket
katax.logger.info({ message: 'Trade executed', broadcast: true, room: 'admins' }); import { RedisTransport, TelegramTransport, CallbackTransport } from 'katax-service-manager';
// Redis transport
const redisTransport = new RedisTransport(katax.db('cache'), {
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(); // 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 |
ws.onConnection((socket) => {
socket.on('message', (data) => { ... });
socket.emit('welcome', { status: 'connected' });
socket.join('room-123');
socket.leave('room-123');
}); 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();
katax.cronService.startJob('process-assets');
katax.cronService.stopJob('process-assets');
katax.cronService.removeJob('process-assets');
katax.cronService.stopAll(); 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 const health = await katax.healthCheck();
// { status: 'healthy' | 'degraded' | 'unhealthy',
// services: { databases: {}, sockets: {}, cron: boolean },
// timestamp: number } 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(); // 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(); import dotenv from 'dotenv';
dotenv.config();
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: '*' } });
katax.cron({ name: 'cleanup', schedule: '0 3 * * *', task: async () => { /* nightly */ } });
httpServer.listen(PORT, () => {
katax.logger.info({ message: 'Server running on http://localhost:' + PORT });
});
} catch (err) {
console.error('Bootstrap failed:', err);
process.exit(1);
}
}
void bootstrap(); CLI for scaffolding Express + TypeScript APIs, generating CRUD/endpoints/repositories, OpenAPI docs, and managing VPS deployments with PM2.
Complete Express + TypeScript project with interactive prompts for DB, auth, validation, Swagger, WebSocket, Redis, and deployment config.
Generates validators, controllers, handlers, and routes. Nested resources supported. Auto-regenerates OpenAPI docs.
Full VPS deployment workflow: init (clone + PM2), update (pull + restart), rollback, logs, and status.
katax init [project-name]Scaffolds a complete Express + TypeScript + katax-core project.
| Flag | Description |
|---|---|
-f, --force | Overwrite existing directory |
--pm <npm|pnpm> | Package manager (default: pnpm) |
--ignore-scripts | Disable lifecycle scripts |
--write-npmrc | Write .npmrc for reproducible installs |
katax add endpoint <name>Add a new endpoint with validator, controller, handler, and routes.
| Flag | Description |
|---|---|
-m, --method <method> | HTTP method (GET, POST, PUT, PATCH, DELETE) |
-p, --path <path> | Custom route path |
katax generate crud <resource-name>Aliases: gen, g. Generates CRUD endpoints: list, get by ID, create, update, delete.
| Flag | Description |
|---|---|
--no-auth | Skip authentication middleware |
katax generate repository <name>Data access layer with typed methods: findAll(), findById(), exists(), create(), update(), delete().
katax generate docs| 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 infoAliases: status, ls. Shows project structure, dependencies, and routes.
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 katax fix docs # Patch build script to copy openapi.json
--skip-install
katax fix all # Apply all fixes
katax fix list # List available patches --no-color # Disable colored output
--verbose # Enable verbose logging
-v, --version # Show version katax init my-api --pm pnpm --ignore-scripts --write-npmrc
katax add endpoint admin/audit/logs -m POST
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 k.object() schemas with per-field validation, async validators and kataxInfer inference.
Business logic with ControllerResult<T>, createSuccessResult(), createErrorResult().
Express middleware chaining validator + controller with sendResponse().
Express Router with method calls and JSDoc documentation.
AST-based update of main routes.ts. Prevents duplicates.
Scans validators and routes, generates OpenAPI 3.0 spec.
sendSuccess<T>()sendError() · sendValidationError()sendResult<T,E>()sendResponse()initSSE() · sendSSEEvent()sendSSEComment() · closeSSE()SSEStream classsendChunked() · streamAsyncIterator() · streamArray()hashPassword() (bcrypt) · hashPasswordArgon2()generateAccessToken() · generateRefreshToken()authenticateToken middlewarerequireRole(...roles) middlewareControllerResult<T> typevalidateSchema()sendResponse()// Result<T, E> pattern
import { ok, err, isOk, isErr, map, mapErr, flatMap,
unwrap, unwrapOr, tryCatch, tryCatchAsync, combine, match } from './core/result.js';
// AppError hierarchy
import { AppError, ValidationError, AuthenticationError,
AuthorizationError, NotFoundError, ConflictError,
DatabaseError, ExternalServiceError, isAppError } from './core/errors.js'; The Copilot/AI skill for the Katax ecosystem. Knows versions, exports, patterns, CLI commands and troubleshooting.