Common
@hopak/common — shared error classes, logger, HTTP status codes. Used by every other @hopak/* package.
Shared primitives for Hopak.js — the error hierarchy, logger, HTTP status codes, filesystem helpers, utilities, and config types. Used by every other @hopak/* package.
You probably don’t need to install this directly.
@hopak/corealready depends on@hopak/commonand re-exports its entire public surface. Install this package only when building your own Hopak plug-in or extension.
Install
bun add @hopak/common
Everything exported here is also available from @hopak/core:
// equivalent imports
import { HopakError, NotFound, HttpStatus, createLogger } from '@hopak/core';
import { HopakError, NotFound, HttpStatus, createLogger } from '@hopak/common';
Errors
All framework errors extend HopakError. Throwing one inside a route handler produces a structured JSON response with the correct status code.
Hierarchy
import {
HopakError, // base — status 500
ValidationError, // 400
Unauthorized, // 401
Forbidden, // 403
NotFound, // 404
Conflict, // 409
RateLimited, // 429
InternalError, // 500
ConfigError, // 500 — config-layer problems
} from '@hopak/common';
Shape
Every HopakError has:
status: number— HTTP status codecode: string— machine-readable error code (e.g."NOT_FOUND")message: string— human-readable messagedetails?: unknown— optional payload (used byValidationErrorfor field errors)toJSON()— the response body
Usage
throw new NotFound('Post not found');
// → 404 { "error": "NOT_FOUND", "message": "Post not found" }
throw new ValidationError('Invalid body', { email: ['Required'] });
// → 400 { "error": "VALIDATION_ERROR", "message": "Invalid body",
// "details": { "email": ["Required"] } }
See Errors for full usage.
Logger
Interface
export interface Logger {
debug(message: string, meta?: LogMeta): void;
info(message: string, meta?: LogMeta): void;
warn(message: string, meta?: LogMeta): void;
error(message: string, meta?: LogMeta): void;
child(bindings: LogMeta): Logger;
}
LogMeta is Record<string, unknown> | object — any plain object works.
ConsoleLogger
The bundled implementation writes JSON-tagged lines to stdout (or stderr for errors). Colour and timestamp included.
import { createLogger } from '@hopak/common';
const log = createLogger({ level: 'debug' });
log.info('server up', { port: 3000 });
log.error('DB connection failed', { cause: 'ECONNREFUSED' });
Child loggers
Pre-bind context on an existing logger:
const reqLog = log.child({ requestId: '123' });
reqLog.info('handler called');
// → [2026-...] INFO handler called {"requestId":"123"}
Plugging your own logger in
Logger is a plain interface — anything that satisfies it works. Pipe to pino, winston, or a custom transport.
HTTP status codes
import { HttpStatus } from '@hopak/common';
HttpStatus.Ok; // 200
HttpStatus.Created; // 201
HttpStatus.NoContent; // 204
HttpStatus.BadRequest; // 400
HttpStatus.Unauthorized; // 401
HttpStatus.Forbidden; // 403
HttpStatus.NotFound; // 404
HttpStatus.MethodNotAllowed; // 405
HttpStatus.Conflict; // 409
HttpStatus.TooManyRequests; // 429
HttpStatus.InternalServerError; // 500
Exported as a const object plus a matching union type — use the object for values, the type as the annotation:
import { HttpStatus } from '@hopak/common';
function respond(status: HttpStatus) { /* ... */ }
respond(HttpStatus.Created);
Filesystem helpers
Async wrappers around node:fs/promises that return false instead of throwing when the path is missing.
import { pathExists, isFile, isDirectory } from '@hopak/common';
await pathExists('./hopak.config.ts'); // true | false
await isFile('./README.md'); // true only if a regular file
await isDirectory('./app/models'); // true only if a directory
Utilities
slugify(input)
slugify('Hello, World!') // 'hello-world'
slugify('Привіт світ') // '' (ASCII-only; non-latin input yields empty)
Lowercases, trims, strips non-word characters, collapses whitespace to -.
pluralize(word)
Simple English pluraliser used by the CRUD scaffolder to form URL segments:
pluralize('post') // 'posts'
pluralize('story') // 'stories'
pluralize('box') // 'boxes'
parseDuration(input)
Parses "100ms", "5s", "10m", "1h", "7d" into milliseconds:
parseDuration('5s') // 5000
parseDuration('2h') // 7200000
Throws on unknown units or malformed input.
deepMerge(target, source)
Recursively merges plain objects. Arrays and primitives in source replace those in target. undefined in source is ignored. Used by the config layer to apply user overrides on top of defaults.
deepMerge({ a: { x: 1, y: 2 } }, { a: { y: 20, z: 30 } });
// { a: { x: 1, y: 20, z: 30 } }
Config types
import type {
HopakConfig,
HopakConfigInput,
HopakPaths,
ServerOptions,
HttpsOptions,
DatabaseOptions,
CorsOptions,
DbDialect,
RuntimeContext,
DeepPartial,
} from '@hopak/common';
HopakConfig— fully-resolved config object (what the framework sees at runtime)HopakConfigInput—DeepPartial<HopakConfig>; the shape you pass todefineConfig({...})HopakPaths— resolvedmodels/routes/jobs/public/migrations/hopakDirdirectoriesServerOptions—{ port, host, https? }HttpsOptions—{ enabled, port?, cert?, key? }DatabaseOptions—{ dialect, url?, file? }CorsOptions—{ origins, credentials? }DbDialect—'sqlite' | 'postgres' | 'mysql'RuntimeContext—{ log, config }; useful when writing plug-insDeepPartial<T>— recursivePartial
License
MIT.