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

@rudderjs/core

v1.1.3

Published

Application bootstrap, service provider lifecycle, and framework-level runtime orchestration.

Readme

@rudderjs/core

Application bootstrap, service provider lifecycle, and framework-level runtime orchestration.

Installation

pnpm add @rudderjs/core

Usage

import { Application } from '@rudderjs/core'
import { hono } from '@rudderjs/server-hono'
import { RateLimit } from '@rudderjs/middleware'

export default Application.configure({
  server:    hono(configs.server),
  config:    configs,
  providers,
})
  .withRouting({
    web:      () => import('../routes/web.js'),
    api:      () => import('../routes/api.js'),
    commands: () => import('../routes/console.js'),
  })
  .withMiddleware((m) => {
    // Global — runs on every request
    m.use(RateLimit.perMinute(60))

    // Group-scoped — only runs on routes loaded via withRouting({ web } / { api })
    m.web(CsrfMiddleware())
    m.api(RateLimit.perMinute(120))
  })
  .withExceptions((e) => {
    // Custom error type → custom response
    e.render(PaymentError, (err) =>
      new Response(JSON.stringify({ code: err.code }), {
        status: 402,
        headers: { 'Content-Type': 'application/json' },
      })
    )
    // Override the reporter (default: @rudderjs/log when installed, otherwise console.error)
    e.reportUsing((err) => Sentry.captureException(err))
    // Re-throw to the server's native fallback
    e.ignore(DebugOnlyError)
  })
  .create()

API Reference

  • ServiceProviderregister(), boot(), publishes()
  • PublishGroup{ from, to, tag? }
  • getPublishGroups() — returns the global publish registry (used by vendor:publish)
  • Listener, EventDispatcher, dispatcher, dispatch(), eventsProvider()
  • Application, AppConfig
  • ConfigureOptions, RoutingOptions
  • MiddlewareConfigurator, ExceptionConfigurator
  • appendToGroup(group, handler) — provider-facing helper to install middleware into the web or api group
  • AppBuilder, RudderJS
  • app(), resolve()
  • defineConfig()
  • HttpException — HTTP error with statusCode, message, headers
  • abort(status, message?, headers?) — throws HttpException
  • abort_if(condition, status, message?) — conditional abort
  • abort_unless(condition, status, message?) — inverse conditional abort
  • report(err) — report an error to the configured reporter
  • report_if(condition, err) — conditional report
  • setExceptionReporter(fn) — override the global reporter (wired automatically by @rudderjs/log)
  • Re-exports from @rudderjs/console, @rudderjs/support, and @rudderjs/contracts types plus built-in DI and Events primitives

Configuration

  • AppConfig
    • name?, env?, debug?
    • providers?
    • config? (config object bound into the container)
  • ConfigureOptions
    • server, config?, providers?

Container

The DI Container is the heart of the framework — services bound here are resolvable via @Inject(), contextual bindings (when().needs().give()), and direct make() calls. Beyond bind / singleton / scoped / instance, two convenience surfaces help framework providers and plugin-style fan-out:

Conditional binding (*If)

bindIf / singletonIf / scopedIf register a binding only when the token is currently unbound. Lets framework providers register a sane default that an app provider can override by binding the same token first:

// Inside CacheServiceProvider.register()
this.app.singletonIf(CacheManager, c => new CacheManager(c.make(ConfigRepo)))

If an app provider already bound CacheManager before this provider registers, the framework default is skipped — no ad-hoc if (!app.has(...)) dance.

Tagging

Group bindings under one or more tag names, then resolve them all at once. Useful for plugin fan-out — exporters, channels, recorders:

container.bind('csv.exporter',  () => new CsvExporter())
container.bind('xlsx.exporter', () => new XlsxExporter())
container.bind('json.exporter', () => new JsonExporter())

container.tag(['csv.exporter', 'xlsx.exporter', 'json.exporter'], 'reports.exporters')
// or, additively:
container.tag('json.exporter', ['reports.exporters', 'serializers.json'])

const exporters = container.tagged<Exporter>('reports.exporters')
// → [CsvExporter, XlsxExporter, JsonExporter] — resolved in insertion order

tagged() returns [] for unknown tags (no throw). Singletons stay singletons across tagged() calls. Tagging an unbound token is allowed — tagged() will throw the standard "cannot resolve" error when one is asked for, matching Laravel's behavior.

For constructor-time injection, decorate a parameter with @Tag(name):

import { Injectable, Tag } from '@rudderjs/core'

@Injectable()
class ReportRunner {
  constructor(@Tag('reports.exporters') private exporters: Exporter[]) {}
}

For contextual binding, pair the tagToken() sentinel with when().needs().give():

import { tagToken } from '@rudderjs/core'

container.when(ReportRunner)
  .needs(tagToken('reports.exporters'))
  .give(c => c.tagged<Exporter>('reports.exporters').filter(e => e.enabled))

@Tag is constructor-only — design:paramtypes metadata is dropped on method parameters by esbuild/Vite.

Extending bindings

extend() wraps the resolved value with a decorator function. Useful for telemetry, tracing, or feature flags without subclassing:

container.singleton(Logger, () => new ConsoleLogger())

container.extend<Logger>(Logger, (logger, c) =>
  new TelescopeLoggerProxy(logger, c.make(Telescope))
)

Multiple extend() calls chain in registration order. Singletons cache the wrapped value (extenders run once); transient bindings re-wrap on every make(); scoped bindings re-wrap once per scope. If a value is already cached when extend() is called, the new extender wraps it eagerly so consumers that already resolved the token see the wrap on their next make().

Rebinding hooks

rebinding() registers a listener that fires when an existing binding is replaced — useful for test hot-swaps and app->refresh() parity:

container.singleton(Mailer, () => new SesMailer())

container.rebinding<Mailer>(Mailer, (newInstance, c) => {
  c.make(MailQueue).rewire(newInstance)
})

// In a test:
container.instance(Mailer, new FakeMailer())   // listener fires synchronously with the FakeMailer

Listeners do not fire on the initial bind — only when an already-bound token is rebound via bind / singleton / scoped / instance. The listener receives the freshly-resolved value, not the stale singleton cache.

Middleware Groups

Routes loaded via withRouting({ web }) are tagged web; via withRouting({ api }) tagged api. The server adapter prepends the matching group's middleware stack before per-route middleware — Laravel-style.

.withMiddleware((m) => {
  m.use(RateLimit.perMinute(60))   // global — every request
  m.web(CsrfMiddleware())           // only on web routes
  m.api(RateLimit.perMinute(120))   // only on api routes
})

Execution order: m.use(...) → group (m.web / m.api) → per-route middleware → handler.

Framework packages install into a group during boot() via appendToGroup('web', handler) from @rudderjs/core — this is how @rudderjs/session and @rudderjs/auth keep session + user resolution on web routes only, leaving api routes stateless by default.

import { ServiceProvider, appendToGroup } from '@rudderjs/core'

export class MyPackageProvider extends ServiceProvider {
  async boot() {
    appendToGroup('web', myWebOnlyMiddleware)
  }
}

Dynamic Provider Registration

Providers can register other providers at runtime — useful for modules, conditional features, and package composition:

import { ServiceProvider } from '@rudderjs/core'
import { CacheProvider } from '@rudderjs/cache'

export class AppServiceProvider extends ServiceProvider {
  register() {
    // Static sub-provider
  }

  async boot() {
    // Conditional features — register a sub-provider only when configured
    const config = this.app.make<{ get(k: string): unknown }>('config')
    if (config.get('cache.enabled')) {
      await this.app.register(CacheProvider)
    }
  }
}

register() calls the provider's register() immediately so bindings are available. If the app is already booted, boot() runs too. Duplicate providers (by class reference or class name) are silently skipped.

Publishing Assets

Service providers can declare publishable assets (pages, config files, migrations) that users copy into their app with pnpm rudder vendor:publish.

import { ServiceProvider } from '@rudderjs/core'

export class MyPackageServiceProvider extends ServiceProvider {
  register(): void {}

  async boot(): Promise<void> {
    this.publishes({
      from: new URL('../pages', import.meta.url).pathname,
      to:   'pages/(panels)',
      tag:  'my-package-pages',
    })
  }
}

Multiple groups with different tags:

this.publishes([
  { from: new URL('../pages', import.meta.url).pathname, to: 'pages/(panels)', tag: 'my-pages' },
  { from: new URL('../config', import.meta.url).pathname, to: 'config',        tag: 'my-config' },
])

Users publish with:

pnpm rudder vendor:publish --tag=my-package-pages
pnpm rudder vendor:publish --provider=MyPackageServiceProvider
pnpm rudder vendor:publish --list   # see all available assets

Events

import { dispatch, dispatcher, eventsProvider } from '@rudderjs/core'

// Define an event
class UserCreated {
  constructor(public readonly id: number) {}
}

// Define a listener
class SendWelcomeEmail {
  async handle(event: UserCreated) {
    await mailer.send(event.id)
  }
}

// Register via provider in bootstrap/providers.ts
import { eventsProvider } from '@rudderjs/core'
export default [
  eventsProvider({ UserCreated: [SendWelcomeEmail] }),
]

// Dispatch anywhere
await dispatch(new UserCreated(42))

EventDispatcher API

| Method | Description | |--------|-------------| | register(name, ...listeners) | Register listeners for an event name. Use '*' for wildcard (all events). | | dispatch(event) | Dispatch to matching listeners, then wildcard listeners. Awaited in order. | | count(name) | Number of listeners for an event name. | | hasListeners(name) | true if at least one listener is registered. | | list() | Record<string, number> snapshot of all registered events and counts. | | reset() | Clear all listeners (testing / hot-reload). |

Testing events (EventFake)

import { EventFake, dispatch } from '@rudderjs/core'

const fake = EventFake.fake()
await dispatch(new UserCreated(42))

fake.assertDispatched('UserCreated')
fake.assertDispatchedTimes('UserCreated', 1)
fake.assertNotDispatched('OrderPlaced')
fake.restore() // always call in afterEach

Exception Handling

abort() helpers

Throw an HttpException from anywhere — routes, services, middleware:

import { abort, abort_if, abort_unless } from '@rudderjs/core'

abort(404)                            // throws HttpException(404, 'Not Found')
abort(403, 'Insufficient permissions')
abort(402, 'Payment required', { 'X-Upgrade-URL': '/billing' })

abort_if(!user, 401)                  // abort if condition is true
abort_unless(user.isAdmin, 403)       // abort if condition is false

HttpException is caught automatically and rendered as JSON or HTML based on the request's Accept header — no try/catch needed.

report() helpers

Manually report an error without aborting the request:

import { report, report_if } from '@rudderjs/core'

report(new Error('Stripe webhook failed'))
report_if(payment.failed, payment.error)

When @rudderjs/log is installed, report() routes through the log channel automatically. Otherwise it falls back to console.error.

withExceptions configurator

.withExceptions((e) => {
  // Custom error type → custom Response
  e.render(PaymentError, (err, req) =>
    Response.json({ code: err.code }, { status: 402 })
  )

  // Override the reporter (default: @rudderjs/log or console.error)
  e.reportUsing((err) => Sentry.captureException(err))

  // Re-throw to the server's native fallback handler
  e.ignore(DebugOnlyError)
})

Built-in handling (no configuration needed)

| Error type | Response | |---|---| | ValidationError | 422 JSON { message, errors } | | ValidationResponse | The wrapped Response is emitted directly (used by FormRequest.failedValidation() short-circuit) | | HttpException | Status from statusCode, JSON or HTML based on Accept | | Unhandled error | Reported via reporter, then 500 (with stack in debug mode) |

FormRequest

Subclass FormRequest, define a Zod schema in rules(), and call validate(req). The merged body + query + params flows through five optional lifecycle hooks that mirror Laravel's FormRequest:

import { FormRequest, z } from '@rudderjs/core'

const schema = z.object({
  email:    z.string().email(),
  password: z.string().min(8),
})

class StoreUser extends FormRequest<typeof schema> {
  rules() { return schema }

  // Mutate input before parsing — sync only.
  protected override prepareForValidation(input: Record<string, unknown>) {
    if (typeof input['email'] === 'string') input['email'] = input['email'].toLowerCase().trim()
  }

  // Per-request message overrides keyed by dot-path. Static string OR function.
  protected override messages() {
    return {
      email:    'Please enter a valid email address.',
      password: (issue: z.core.$ZodRawIssue) =>
        issue.code === 'too_small' ? 'Min 8 characters.' : 'Invalid password.',
    }
  }

  // Cross-field checks against parsed data. Run serially after parse; collect all errors.
  protected override after() {
    return [
      ({ data, addError }) => {
        if (data.email.endsWith('@example.com')) addError('email', 'No example.com addresses')
      },
    ]
  }

  // Final transform after all checks pass; return value replaces resolved data.
  protected override async passedValidation(data: z.infer<typeof schema>) {
    return { ...data, password: await Bcrypt.hash(data.password) }
  }

  // Customize the failure path. Default throws `ValidationError`; return a `Response` to short-circuit.
  protected override failedValidation(errors: Record<string, string[]>): never {
    throw new ValidationError(errors)
  }
}

Pipeline order: prepareForValidation → authorize → rules.parse → after → passedValidation. Both Zod parse failures and after() errors converge through failedValidation(errors).

Short-circuit responses: failedValidation may return a Web Response to bypass the default 422 — the framework's exception handler unwraps the ValidationResponse sentinel and emits the wrapped Response directly.

Type inference: parameterize the class with the schema type (extends FormRequest<typeof schema>) so data in after()/passedValidation is inferred as z.infer<typeof schema>. Without the parameter, data is typed as unknown.

Notes

  • Application.create() is singleton-based and can recreate in development/local mode when config is passed.
  • RudderJS.boot() boots providers; RudderJS.handleRequest() lazily creates the HTTP handler.
  • ValidationError is always caught and returned as 422 JSON — no try/catch needed in routes.
  • HttpException is always caught and rendered with its status code — no try/catch needed in routes.
  • Unhandled errors are auto-reported and render as 500. In debug mode the response includes the exception message and stack trace.