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

typeroad

v4.0.0-alpha.0

Published

TypeScript backend framework — DI Container, Filter Chain, EventBus, Provider Pattern

Readme

TypeRoad

TypeScript backend framework for multi-tenant Express APIs — DI Container, Filter Chain, EventBus, Provider Pattern.

v4.0.0-alpha.0 — Full TypeScript rewrite of the onRoad framework.


Table of Contents


Installation

npm install typeroad

Peer dependencies (install as needed):

npm install express reflect-metadata pino uuid
# Optional — only if using HttpTransport:
npm install axios

TypeRoad is ESM-only ("type": "module"). Your tsconfig.json must use "module": "NodeNext" or "Node16" and enable "experimentalDecorators": true.


Quick Start

import "reflect-metadata"
import { OnRoadExpress, Controller, Service, Repository } from "typeroad"
import { SequelizeConnectionManager } from "typeroad/database"
import { CorsFilter, JwtFilter, TenantFilter, RoleFilter } from "typeroad/filters"

// 1. Define your layers
@Repository()
class OrdemRepository { /* ... */ }

@Service()
class OrdemService { /* ... */ }

@Controller("/ordem")
class OrdemController {
  /* ... */
}

// 2. Create the app
const app = new OnRoadExpress({
  connections: [
    new SequelizeConnectionManager({ dialect: "postgres", host: "localhost", database: "mydb" }),
  ],
})

// 3. Register filters
app.useFilters([
  new CorsFilter({ origins: ["http://localhost:3000"] }),
  new JwtFilter({ secret: process.env.JWT_SECRET! }),
  new TenantFilter(),
  new RoleFilter(),
])

// 4. Register modules
app.register([OrdemController, OrdemService, OrdemRepository])

// 5. Build & start
await app.buildServer({ port: 3001 })

Architecture Overview

┌──────────────────────────────────────────────────┐
│                  OnRoadExpress                   │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐ │
│  │  Container  │  │ FilterChain│  │  EventBus  │ │
│  │  (DI + IoC) │  │ (Middleware)│  │ (Sentinel) │ │
│  └─────┬──────┘  └─────┬──────┘  └─────┬──────┘ │
│        │               │               │        │
│  ┌─────▼───────────────▼───────────────▼──────┐ │
│  │         Request Handler (route)             │ │
│  │  requestContext.run({ tenant, appToken })   │ │
│  │  ┌──────────┐ ┌─────────┐ ┌────────────┐   │ │
│  │  │Controller│→│ Service │→│ Repository │   │ │
│  │  └──────────┘ └─────────┘ └────────────┘   │ │
│  └─────────────────────────────────────────────┘ │
│                                                  │
│  ┌──────────────────────────────────────────┐   │
│  │              Providers                    │   │
│  │  Messaging │ Realtime │ TaskSched │ Socket│   │
│  └──────────────────────────────────────────┘   │
│                                                  │
│  ┌──────────────────────────────────────────┐   │
│  │           TransportFactory                │   │
│  │     HttpTransport │ MessagingTransport    │   │
│  └──────────────────────────────────────────┘   │
└──────────────────────────────────────────────────┘

DI Container & Decorators

TypeRoad uses a decorator-based DI container with automatic class scanning.

Decorators

| Decorator | Scope | Purpose | |-----------|-------|---------| | @Injectable() | Transient (default) | Generic injectable class | | @Controller("/path") | Request | Express route handler | | @Service() | Request | Business logic layer | | @Repository() | Request | Data access layer |

Scopes

| Scope | Behavior | |-------|----------| | SINGLETON | One instance for the entire application | | REQUEST | One instance per request scope | | TRANSIENT | New instance on every resolve |

Usage

import { Injectable, Scope } from "typeroad"

@Injectable({ scope: Scope.SINGLETON })
class CacheManager {
  private store = new Map<string, unknown>()
  get(key: string) { return this.store.get(key) }
  set(key: string, value: unknown) { this.store.set(key, value) }
}

Register all classes via app.register([...]) — the container auto-scans metadata.


Controllers, Services & Repositories

AbstractController

import { Controller, AbstractController, type RouteConfig } from "typeroad"

@Controller("/ordem")
class OrdemController extends AbstractController<OrdemService> {
  constructor() {
    super({
      service: OrdemService,
      routes: {
        findAll: { method: "get" },
        create:  { method: "post" },
        update:  { method: "put", path: "/:id" },
        remove:  { method: "delete", path: "/:id" },
      },
    })
  }

  async findAll(req: Request, res: Response) {
    const data = await this.service.findAll(req.query)
    res.json({ content: data })
  }

  async create(req: Request, res: Response) {
    const sentinel = new Sentinel(req, res)
    const result = await this.service.create(req.body, sentinel)
    await sentinel.finishRequest(result)
  }
}

AbstractService

import { Service, AbstractService } from "typeroad"

@Service()
class OrdemService extends AbstractService<OrdemRepository> {
  constructor() {
    super({ repository: OrdemRepository })
  }

  async findAll(query: unknown) {
    return this.repository.findAll(query)
  }
}

SequelizeRepository / MongooseRepository

import { Repository, SequelizeRepository } from "typeroad"

@Repository()
class OrdemRepository extends SequelizeRepository {
  constructor() {
    super({ modelName: "Ordem" })
  }
}

Entity System

Decorate model classes with metadata for auto-registration:

import { Entity, Column, DataType, HasMany } from "typeroad/entity"

@Entity({ tableName: "ordens", timestamps: true })
class Ordem {
  @Column({ type: DataType.UUID, primaryKey: true })
  id!: string

  @Column({ type: DataType.STRING, allowNull: false })
  descricao!: string

  @Column({ type: DataType.ENUM("aberta", "fechada") })
  status!: string

  @HasMany(() => Tarefa, { foreignKey: "ordemId" })
  tarefas!: Tarefa[]
}

The EntityRegistry collects all decorated entities for model initialization.


Filter Chain

Filters are ordered middleware executed before route handlers. TypeRoad includes 5 built-in filters:

| Filter | Order | Purpose | |--------|-------|---------| | CorsFilter | 10 | CORS headers | | JwtFilter | 20 | JWT token validation | | TenantFilter | 30 | Multi-tenant resolution | | RoleFilter | 40 | Role-based access | | RequestContextFilter | 50 | Attach logger/context to request |

Registration (3 forms)

// 1. Bare instance
app.useFilters([new CorsFilter({ origins: ["*"] })])

// 2. Class reference (uses @Filter decorator config)
app.useFilters([CorsFilter])

// 3. Object form (manual config overrides decorator)
app.useFilters([{
  filter: new JwtFilter({ secret: "..." }),
  order: 15,
  exclude: ["/health", "/public"],
}])

Custom Filter

import { OnRoadFilter, Filter, FilterChain } from "typeroad/filters"

@Filter({ order: 25 })
class AuditFilter extends OnRoadFilter {
  async execute(req: Request, res: Response, chain: FilterChain) {
    console.log(`[${req.method}] ${req.path}`)
    await chain.next(req, res) // pass to next filter
  }
}

// Auto-discovered when passed to app.register()
app.register([AuditFilter])

Filters support exclude (skip specific paths) and condition (dynamic enable/disable).


Security (Roles & Public)

@Roles

Restrict access at class or method level:

import { Roles, Public } from "typeroad/security"

@Roles("admin", "manager")
@Controller("/config")
class ConfigController extends AbstractController<ConfigService> {
  // Inherits class-level roles: admin, manager

  @Roles("admin") // Method-level overrides class-level
  async deleteAll(req: Request, res: Response) { /* ... */ }

  @Public() // No role check — open access
  async getVersion(req: Request, res: Response) { /* ... */ }
}
  • Method-level @Roles overrides class-level (not additive).
  • @Public() sets roles to [] (empty array = no restriction).
  • getRoles(instance, methodName) returns undefined if no decorator is present (no enforcement).

OnRoadExpress enforces roles automatically in buildServer() by reading req.decoded.roles or req.user.roles.


Sentinel & EventBus

The Sentinel manages the request lifecycle — transactions, errors, and events.

async create(req: Request, res: Response) {
  const sentinel = new Sentinel(req, res)

  // Start database transaction
  sentinel.transaction = await sequelize.transaction()

  try {
    const result = await this.service.create(req.body, sentinel)

    // Queue side-effects
    sentinel.appendMo({ type: "ordemCriada", data: result })
    sentinel.appendNotification({ userId: "abc", title: "Nova OS", body: "..." })

    // Commit + fire afterCommit events + send response
    await sentinel.finishRequest(result)
  } catch (err) {
    sentinel.appendError(err as Error)
    await sentinel.finishRequest(null) // triggers rollback + 500
  }
}

EventBus Handlers

sentinel.eventBus.on("afterCommit", async (payload) => {
  // Publish matching objects to RTDB
  // Send notifications
  // Propagate to other APIs
})

sentinel.eventBus.on("matchingObject", (mo) => {
  // Process individual matching object
})

Providers

TypeRoad defines 4 abstract provider interfaces. Each has an isReady() method and lifecycle hooks.

| Provider | Purpose | Example Implementation | |----------|---------|----------------------| | MessagingProvider | Inter-API async messaging | RabbitMQ (CloudAMQP) | | RealtimeProvider | Push updates to frontends | Firebase RTDB | | TaskSchedulerProvider | Delayed/scheduled tasks | Google Cloud Tasks | | SocketProvider | WebSocket communication | Socket.io |

Registration

const app = new OnRoadExpress()

app.setMessagingProvider(new RabbitMQMessagingProvider(process.env.AMQP_URL!))
app.setRealtimeProvider(new FirebaseRealtimeProvider(firebaseApp))
app.setTaskSchedulerProvider(new CloudTasksProvider(config))
app.setSocketProvider(new SocketIOProvider(httpServer))

All providers fail gracefully during buildServer() — if one fails to initialize, the app continues without it.

Implementing a Provider

import { MessagingProvider, type MessagingMessage, type MessageWorker } from "typeroad"

class RabbitMQMessagingProvider extends MessagingProvider {
  isReady(): boolean { return this.connected }

  async initialize(config: Record<string, unknown>): Promise<void> {
    // Connect to RabbitMQ...
  }

  async publish(message: MessagingMessage): Promise<void> {
    // Publish to exchange...
  }

  async subscribe(route: string, worker: MessageWorker): Promise<void> {
    // Bind queue and consume...
  }

  async shutdown(): Promise<void> {
    // Close connection...
  }
}

Inter-Service Transport

Transport classes enable API-to-API communication with zero boilerplate.

HttpTransport

// Register API endpoints
app.registerApiClients({
  Manutencao: "http://api-manutencao:3001",
  Processo:   "http://api-processo:3002",
})

// Inside a service — just call it
const transport = app.transportFactory.createHttp("Manutencao")
const result = await transport.send("/ordem/sync", { data: payload })

MessagingTransport

const transport = app.transportFactory.createMessaging()
await transport.send("ordem.created", { ordemId: "123" })

No Sentinel or tenant needs to be passed — the transport reads them automatically from the active RequestContext via AsyncLocalStorage.


Request Context (AsyncLocalStorage)

Every route handler is wrapped in requestContext.run() by OnRoadExpress, providing request-scoped data without parameter threading:

import { getRequestContext } from "typeroad"

// Anywhere in the call stack during a request:
const ctx = getRequestContext()
console.log(ctx.tenant)    // current tenant
console.log(ctx.appToken)  // x-app-token header
console.log(ctx.logger)    // request-scoped logger (if configured)

This is what enables InterServiceTransport, HttpTransport, and MessagingTransport to automatically include tenant and x-app-token headers without receiving them as constructor arguments.

If called outside a request handler, getRequestContext() throws a descriptive error.


Logging

TypeRoad uses pino via the PinoLogger class implementing the OnRoadLogger interface.

import { PinoLogger } from "typeroad/logging"

const app = new OnRoadExpress({
  logger: new PinoLogger({ level: "debug" }),
})

// Anywhere:
app.logger.info("Server started", { port: 3001 })
app.logger.warn("Slow query", { duration: 2500 })
app.logger.error("Connection failed", { host: "db" })

OnRoadLogger Interface

interface OnRoadLogger {
  info(message: string, meta?: Record<string, unknown>): void
  warn(message: string, meta?: Record<string, unknown>): void
  error(message: string, meta?: Record<string, unknown>): void
  debug(message: string, meta?: Record<string, unknown>): void
  child(bindings: Record<string, unknown>): OnRoadLogger
}

Swap PinoLogger for any custom implementation of OnRoadLogger.


Plugins

Plugins extend OnRoadExpress capabilities. A plugin receives the full app reference during installation.

import type { OnRoadPlugin } from "typeroad"

class MetricsPlugin implements OnRoadPlugin {
  name = "metrics"

  async install(app: { app: Express; eventBus: RequestEventBus }): Promise<void> {
    app.eventBus.on("afterCommit", (payload) => {
      // Track metrics...
    })
  }
}

await app.use(new MetricsPlugin())

@onroad/plugin-notification

The notification plugin is a separate package: @onroad/plugin-notification.

import { NotificationPlugin } from "@onroad/plugin-notification"

await app.use(new NotificationPlugin({
  apiUrl: process.env.NOTIFICATION_API_URL!,
  timeout: 5000,
}))

It listens to afterCommit events and sends notification payloads to the notification API using native fetch with Promise.allSettled for resilience.


Storage

Abstract StorageProvider for file/attachment operations (e.g., GCS, S3):

import { StorageProvider } from "typeroad/storage"

class GCSStorageProvider extends StorageProvider {
  async upload(buffer: Buffer, opts: UploadOptions): Promise<string> { /* ... */ }
  async getSignedUrl(key: string): Promise<string> { /* ... */ }
  async delete(key: string): Promise<void> { /* ... */ }
}

Health Endpoint

GET /health is automatically registered by buildServer():

{
  "status": "ok",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "providers": {
    "messaging": true,
    "realtime": true,
    "taskScheduler": null,
    "socket": null
  }
}
  • true = provider registered and ready
  • false = provider registered but not ready
  • null = provider not registered

Graceful Shutdown

await app.shutdown()

shutdown() disconnects all providers sequentially (messaging, realtime, taskScheduler, socket), closes the HTTP server, and logs each step. Each provider failure is isolated — one failing provider does not block the others.


Testing

TypeRoad uses Vitest with 223 tests across 9 test files:

npm test              # Run all tests
npm run test:watch    # Watch mode
npm run test:coverage # Coverage report

| Test File | Tests | Covers | |-----------|-------|--------| | container.test.ts | 16 | DI Container, decorators, scopes | | entity.test.ts | 14 | Entity, Column, associations | | database.test.ts | 13 | ConnectionManagers | | filters.test.ts | 33 | FilterChain, all 5 built-in filters | | security.test.ts | 25 | @Roles, @Public, enforcement | | sentinel.test.ts | 36 | Sentinel, EventBus, transactions | | logging.test.ts | 20 | PinoLogger, OnRoadLogger | | providers.test.ts | 41 | All 4 providers, graceful-fail, shutdown | | transport.test.ts | 25 | HttpTransport, MessagingTransport, TransportFactory, RequestContext |


Project Structure

src/
├── index.ts                  # Public API exports
├── OnRoadExpress.ts          # Main orchestrator
├── container/                # DI Container + decorators
├── context/                  # RequestContext (AsyncLocalStorage)
├── core/                     # AbstractController/Service/Repository, Sentinel, EventBus
├── entity/                   # @Entity, @Column, @Field, associations
├── filters/                  # FilterChain, @Filter, built-in filters (Cors, JWT, Tenant, Role, RequestContext)
├── security/                 # @Roles, @Public
├── database/                 # ConnectionManager abstract + Sequelize/Mongo implementations
├── transport/                # InterServiceTransport, HttpTransport, MessagingTransport, TransportFactory
├── messaging/                # MatchingObject
├── providers/                # MessagingProvider, RealtimeProvider, TaskSchedulerProvider, SocketProvider
├── logging/                  # OnRoadLogger interface + PinoLogger
├── plugins/                  # OnRoadPlugin interface
├── storage/                  # StorageProvider abstract
└── types/                    # Express Request augmentation

Subpath Exports

Import only what you need:

import { OnRoadExpress } from "typeroad"                           // Main
import { AbstractController, Sentinel } from "typeroad/core"       // Core classes
import { Entity, Column, DataType } from "typeroad/entity"         // Entity decorators
import { FilterChain, CorsFilter } from "typeroad/filters"         // Filters
import { Roles, Public } from "typeroad/security"                  // Security
import { SequelizeConnectionManager } from "typeroad/database"     // Database
import { HttpTransport, TransportFactory } from "typeroad/transport" // Transport
import { MatchingObject } from "typeroad/messaging"                // Messaging
import { PinoLogger } from "typeroad/logging"                      // Logging
import { StorageProvider } from "typeroad/storage"                 // Storage

License

UNLICENSED — HashCodeTI-Brasil