Build on top

Catalogs as packages

Package an error catalog or audit catalog as a reusable npm module — `@my-org/evlog-stripe-errors`, `@my-org/evlog-aws-audit`, etc.

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:

  1. The defined catalog (an object) — for evlog to register
  2. A typed module augmentation — so consumers get autocomplete on the catalog keys
src/index.ts
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:

package.json
{
  "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):

tsdown.config.ts
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:

  1. Stable identifiers — the same code / action lives in one repo, not duplicated across services
  2. Documented errors / actionswhy, fix, severity ride along with the type
  3. 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.