Catalogs as packages
evlog catalogs (error catalogs, audit catalogs) are plain TypeScript objects passed to defineErrorCatalog() / defineAuditCatalog(). They're not tied to a project — anything you can import works. That makes them perfect to publish as reusable npm packages:
- An organization-wide error catalog shared by every microservice
- An open-source catalog for a specific domain (Stripe payments, AWS API errors, RFC 7807 problem types)
- A team's audit catalog covering compliance actions across products
This page shows the minimum scaffolding for a package that exports a catalog and how downstream apps consume it.
What goes in the package
A catalog package exports two things:
- The defined catalog (an object) — for
evlogto register - A typed
moduleaugmentation — so consumers get autocomplete on the catalog keys
import { defineErrorCatalog } from 'evlog'
export const stripeErrors = defineErrorCatalog('stripe', {
card_declined: {
code: 'STRIPE_CARD_DECLINED',
status: 402,
message: 'Card was declined.',
why: 'The issuing bank rejected the charge.',
fix: 'Try a different payment method or contact the bank.',
},
insufficient_funds: {
code: 'STRIPE_INSUFFICIENT_FUNDS',
status: 402,
message: 'Card has insufficient funds.',
why: 'The card balance does not cover this charge.',
fix: 'Try a different card or use a smaller amount.',
},
// ... more entries ...
} as const)
declare module 'evlog' {
interface RegisteredErrorCatalogs {
stripe: typeof stripeErrors
}
}
The as const is what makes the catalog keys propagate as a string literal union into createError('stripe.card_declined' | ...).
The declare module 'evlog' block registers the catalog globally — once a consumer imports your package, every call to createError() in their codebase autocompletes your catalog's entries.
Package layout
A minimal package.json for a catalog package:
{
"name": "@my-org/evlog-stripe-errors",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
}
},
"files": ["dist"],
"peerDependencies": {
"evlog": "^2"
},
"devDependencies": {
"evlog": "^2",
"tsdown": "^0.21",
"typescript": "^6"
}
}
evlog goes in peerDependencies — the consumer brings their version, the catalog just imports types from it.
Build with tsdown (or any bundler that emits dual .mjs + .d.mts):
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: { 'index': 'src/index.ts' },
format: 'esm',
dts: true,
external: ['evlog'],
})
Consuming the package
Downstream apps install and import — no extra wiring beyond the existing evlog setup:
pnpm add @my-org/evlog-stripe-errors
import '@my-org/evlog-stripe-errors' // side-effect: registers the catalog
import { createError } from 'evlog'
throw createError('stripe.card_declined')
// ^^^^^^^^^^^^^^^^^^^^^^^
// autocompletes from the package
Because the augmentation runs at type-check time, the consumer doesn't even need to call defineErrorCatalog themselves — the import is the registration.
For audit catalogs the pattern is identical — replace defineErrorCatalog with defineAuditCatalog and RegisteredErrorCatalogs with RegisteredAuditCatalogs:
import { defineAuditCatalog } from 'evlog'
export const awsAudit = defineAuditCatalog('aws', {
iam_role_assumed: {
actor: 'service',
target: 'aws.iam.role',
severity: 'info',
},
s3_bucket_deleted: {
actor: 'user',
target: 'aws.s3.bucket',
severity: 'high',
},
} as const)
declare module 'evlog' {
interface RegisteredAuditCatalogs {
aws: typeof awsAudit
}
}
Why bother
A catalog package consolidates three things:
- Stable identifiers — the same
code/actionlives in one repo, not duplicated across services - Documented errors / actions —
why,fix,severityride along with the type - Type-level discoverability — consumers see every supported entry in autocomplete
When the catalog grows or evolves (a new error code is added, a fix text is improved), every consuming app picks it up by bumping the version. No string-based registry to keep in sync.
Real examples to build
@my-org/evlog-rfc7807— error catalog matching RFC 7807 problem types@my-org/evlog-stripe-errors— every code returned by Stripe APIs@my-org/evlog-aws-audit— AWS-style audit actions for compliance@my-org/evlog-better-auth-audit— audit catalog for Better Auth flows@my-org/evlog-shopify-errors— translated Shopify error responses
The pattern is the same — pick a domain, encode its identifiers as a catalog, ship it.
Overview
Package catalogs, drains, enrichers, and framework integrations as reusable npm libraries — same scaffolding pattern for each, type-level discoverability flows transitively to consumers.
Drains as packages
Package a custom drain as a reusable npm library — `@my-org/evlog-internal-loki`, `@my-org/evlog-pubsub`, etc. Same scaffolding pattern as catalogs.