npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hexaijs/plugin-contracts-generator

v0.1.0

Published

Collect domain events and commands from bounded contexts and generate frontend-compatible contracts package

Readme

@hexaijs/plugin-contracts-generator

Extract Domain Events, Commands, and Queries from backend source code to generate frontend-compatible contract types

Overview

@hexaijs/plugin-contracts-generator solves the problem of keeping frontend and backend type definitions in sync. In a hexagonal architecture, your backend defines domain events, commands, and queries - but your frontend also needs type-safe access to these message types for API calls, event handling, and validation.

Instead of manually duplicating type definitions (which inevitably drift out of sync), this plugin scans your backend source code for specially decorated classes and extracts them into a standalone contracts package. The generated package contains only the public API surface - the message types and their payloads - without any backend implementation details.

The plugin works at build time by:

  1. Scanning TypeScript files for classes decorated with @PublicEvent(), @PublicCommand(), or @PublicQuery()
  2. Resolving all type dependencies (including response types and shared value objects)
  3. Generating a clean contracts package with namespace exports and a MessageRegistry for deserialization

Installation

npm install @hexaijs/plugin-contracts-generator

Peer dependencies:

  • typescript ^5.0.0

Core Concepts

Decorators

The package provides three decorators that mark messages for extraction. These decorators have no runtime overhead - they simply tag classes for discovery during the build process.

import { PublicEvent, PublicCommand, PublicQuery } from "@hexaijs/plugin-contracts-generator/decorators";

@PublicEvent() - Marks a domain event as part of the public contract:

import { DomainEvent } from "@hexaijs/core";
import { PublicEvent } from "@hexaijs/plugin-contracts-generator/decorators";

@PublicEvent()
export class OrderPlaced extends DomainEvent<{
    orderId: string;
    customerId: string;
    totalAmount: number;
}> {
    static readonly type = "order.order-placed";
}

@PublicCommand() - Marks a command as part of the public contract:

import { PublicCommand } from "@hexaijs/plugin-contracts-generator/decorators";

@PublicCommand()
export class CreateOrderRequest extends BaseRequest<{
    customerId: string;
    items: OrderItem[];
}> {
    static type = "order.create-order";
}

export type CreateOrderResponse = {
    orderId: string;
};

@PublicQuery() - Marks a query as part of the public contract:

import { PublicQuery } from "@hexaijs/plugin-contracts-generator/decorators";

@PublicQuery({ response: "OrderDetails" })
export class GetOrderQuery extends BaseRequest<{
    orderId: string;
}> {}

type OrderDetails = {
    orderId: string;
    status: string;
    items: OrderItem[];
};

Each decorator accepts optional configuration:

  • context - Override the context name for this message
  • version - Specify a version number for versioned events
  • response - Explicitly name the response type (for commands/queries)

Configuration

Create an application.config.ts file in your monorepo root:

// application.config.ts
export default {
    contracts: {
        // Context definitions (required)
        contexts: [
            {
                name: "order",
                sourceDir: "packages/order/src",
                tsconfigPath: "packages/order/tsconfig.json", // optional
            },
            {
                name: "inventory",
                sourceDir: "packages/inventory/src",
            },
        ],

        // Output package configuration (required)
        outputPackage: {
            name: "@myorg/contracts",
            dir: "packages/contracts",
        },

        // Path alias rewrite rules (optional)
        pathAliasRewrites: {
            "@myorg/": "@/",
        },

        // Additional dependencies for contracts package (optional)
        externalDependencies: {
            "@hexaijs/core": "workspace:*",
        },

        // Response type naming conventions (optional)
        responseNamingConventions: [
            { messageSuffix: "Command", responseSuffix: "CommandResult" },
            { messageSuffix: "Query", responseSuffix: "QueryResult" },
            { messageSuffix: "Request", responseSuffix: "Response" },
        ],
    },
};

For monorepos with many packages, use glob patterns to auto-discover contexts:

export default {
    contracts: {
        contexts: ["packages/*"],  // Matches all directories under packages/
        outputPackage: {
            name: "@myorg/contracts",
            dir: "packages/contracts",
        },
    },
};

Each matched package must have its own application.config.ts with contextName and sourceDir.

Response Types

Commands and queries often have associated response types. The generator includes these in the contracts package automatically.

Automatic detection via naming conventions:

// When responseNamingConventions includes { messageSuffix: "Command", responseSuffix: "CommandResult" }

@PublicCommand()
export class CreateOrderCommand extends Message<{ customerId: string }> {}

type CreateOrderCommandResult = {  // Automatically detected by naming pattern
    orderId: string;
};

Explicit response option:

@PublicCommand({ response: "OrderCreationResult" })
export class CreateOrder extends Message<{ customerId: string }> {}

type OrderCreationResult = {
    orderId: string;
    createdAt: Date;
};

Response types must be in the same file as the command/query. Both type aliases and interface declarations are supported. The generator adds export automatically if the type isn't already exported.

Entry vs Dependency Files

The generator handles two types of files differently:

Entry files (files with @Public* decorators) undergo symbol extraction:

  • Only decorated classes matching the specified message types are extracted
  • Handler classes are excluded
  • Response types are included based on naming conventions
  • Unused imports are removed

Dependency files (imported by entry files) are copied entirely:

  • Supports barrel files (export * from './module')
  • Preserves all exports for transitive dependencies
  • Ensures type dependencies remain intact

Usage

CLI

Run the generator from your monorepo root:

# Uses application.config.ts by default
npx contracts-generator

# Specify config file path
npx contracts-generator --config ./application.config.ts

# Filter by message types
npx contracts-generator -m event           # Extract only events
npx contracts-generator -m command         # Extract only commands
npx contracts-generator -m query           # Extract only queries
npx contracts-generator -m event,command   # Extract events and commands

Programmatic API

For custom build scripts:

import { processContext, ConsoleLogger } from "@hexaijs/plugin-contracts-generator";

const result = await processContext({
    contextName: "order",
    sourceDir: "packages/order/src",
    outputDir: "packages/contracts/src",
    pathAliasRewrites: new Map([["@myorg/", "@/"]]),
    messageTypes: ["event", "command"],
    responseNamingConventions: [
        { messageSuffix: "Command", responseSuffix: "CommandResult" },
    ],
    logger: new ConsoleLogger({ level: "info" }),
});

console.log(`Extracted ${result.events.length} events, ${result.commands.length} commands`);

For fine-grained control, use the ContractsPipeline class which provides step-by-step execution: scan(), parse(), resolve(), copy(), and exportBarrel().

Output Structure

The generated contracts package follows this structure:

contracts/
├── src/
│   ├── {context}/
│   │   ├── events.ts
│   │   ├── commands.ts
│   │   ├── types.ts       # Dependent types + Response types
│   │   └── index.ts       # Barrel exports
│   └── index.ts           # Namespace exports + MessageRegistry
├── package.json
└── tsconfig.json

The root index.ts uses namespace exports to prevent name collisions:

// contracts/src/index.ts
import { MessageRegistry } from "@hexaijs/plugin-contracts-generator/runtime";

export * as order from "./order";
export * as inventory from "./inventory";

export const messageRegistry = new MessageRegistry()
    .register(order.OrderPlaced)
    .register(inventory.StockUpdated);

Use namespace exports in your frontend:

import { order, messageRegistry } from "@myorg/contracts";

// Access types via namespace
const event = new order.OrderPlaced({ orderId: "123", customerId: "456" });

// Deserialize messages from the backend
const message = messageRegistry.dehydrate(header, body);

Error Handling

The generator provides specific error types for different failure modes:

import {
    processContext,
    MessageParserError,
    FileReadError,
    ConfigLoadError,
} from "@hexaijs/plugin-contracts-generator";

try {
    await processContext(options);
} catch (error) {
    if (error instanceof FileReadError) {
        console.error(`Failed to read: ${error.path}`, error.cause);
    } else if (error instanceof ConfigLoadError) {
        console.error(`Config error: ${error.message}`);
    } else if (error instanceof MessageParserError) {
        console.error(`Parser error: ${error.message}`);
    }
}

Error hierarchy:

  • MessageParserError (base)
    • ConfigurationErrorConfigLoadError, TsconfigLoadError
    • FileSystemErrorFileNotFoundError, FileReadError, FileWriteError
    • ParseErrorJsonParseError
    • ResolutionErrorModuleResolutionError

API Highlights

| Export | Description | |--------|-------------| | processContext(options) | Main API for extracting and copying contracts | | ContractsPipeline | Fine-grained control over extraction process | | PublicEvent | Decorator to mark events for extraction | | PublicCommand | Decorator to mark commands for extraction | | PublicQuery | Decorator to mark queries for extraction | | MessageRegistry | Runtime registry for message deserialization | | ConsoleLogger | Configurable logger for build output | | Error types | ConfigLoadError, FileReadError, MessageParserError, etc. |

See Also