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

@canlooks/roost

v0.0.1

Published

A backend micro service framework

Readme

@canlooks/roost

A lightweight, decorator-driven microservice framework for Node.js. Built on dependency injection, declarative routing, and schema-based validation — designed to minimize boilerplate and maximize clarity.

Installation

npm install @canlooks/roost

Requires TypeScript with experimentalDecorators and emitDecoratorMetadata enabled in tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Quick Start

import {Roost, Controller, Action} from '@canlooks/roost'

@Controller('api')
class ApiController {
    @Action('hello')
    hello(name: string) {
        return `Hello, ${name}!`
    }
}

const app = await Roost.create({
    anonymous: [ApiController]
})

const [result] = await app.invoke('api/hello', 'World')
console.log(result) // "Hello, World!"

Core Concepts

Application Bootstrap

Roost.create() is the sole entry point. It wires the IoC container, plugin pipeline, and route registry.

const app = await Roost.create({
    named: {cache: CacheService},     // named components
    anonymous: [UserController],         // auto-registered components
    plugins: [loggingPlugin],            // lifecycle plugins
    dtoOptions: {allErrors: true}      // ajv options for validation
})

Components

A component is any class registered with the framework. Components go through a lifecycle pipeline during registration:

instantiate → @Config → @Expect → @Controller/@Action → @Module → @Inject → @Initialize

Routing

Routes are declared with @Controller and @Action decorators. Three routing modes are supported.

Path Routing


@Controller('users')
class UserController {
    @Action('list')
    list() {
        return ['alice', 'bob']
    }

    @Action(':id')
    getById(@Params() params: Params) {
        return {id: params.id}
    }
}

const app = await Roost.create({anonymous: [UserController]})
await app.invoke('users/list')     // → ['alice', 'bob']
await app.invoke('users/42')       // → { id: '42' }

Path wildcards:

| Pattern | Matches | |---------|--------------------------------------------| | * | Exactly one segment | | ** | Zero or more segments | | :name | One segment, captured as a named parameter |

Pattern Routing


@Controller({type: 'rpc'})
class RpcController {
    @Action({method: 'add'})
    add(a: number, b: number) {
        return a + b
    }
}

await app.invoke({type: 'rpc', method: 'add'}, 3, 4) // → [7]

Regular Expression Routing


@Controller()
class ApiController {
    @Action(/^\/api\/v\d+\/health$/)
    health() {
        return {status: 'ok'}
    }
}

Route Invocation

// By path (string)
const results = await app.invoke('users/42', ...args)

// By pattern (object)
const results = await app.invoke({type: 'rpc', method: 'add'}, ...args)

Dependency Injection

@Inject supports four injection strategies:

class UserService {
    // Inject the app instance itself
    @Inject(app)
    app!: Roost

    // Inject by component class (auto-registered + singleton)
    @Inject(CacheService)
    cache!: CacheService

    // Inject by registered name
    @Inject('logger')
    logger!: Logger

    // Lazy-load via dynamic import
    @Inject(() => import('./heavy-module'))
    heavy!: HeavyModule
}

Modules

@Module declares sub-component dependencies. The framework recursively registers all declared components.

class AuthService {
}

class Logger {
}

// Anonymous array form
@Module([AuthService, Logger])
class AppModule {
}

// Named object form
@Module({auth: AuthService, log: Logger})
class AppModule {
}

Configuration

@Config injects default property values at registration time.


@Config({timeout: 3000, retries: 3})
class ApiClient {
    timeout!: number
    retries!: number
}

// Or as a higher-order function:
Config(ApiClient, {timeout: 5000})

Values are deeply cloned per instance via structuredClone, so object configs are safe from cross-instance mutation.


Lifecycle Hooks

@Initialize marks methods that run after all dependencies are injected.

class DatabaseService {
    @Inject('config')
    config!: Config

    @Initialize()
    async connect() {
        await this.driver.connect(this.config.url)
    }
}

Multiple @Initialize methods run in parallel via Promise.all.


DTO Validation

Built on ajv. Define schemas with decorators and validate data at registration time or invocation time.

Schema Definition


@DTO()
class CreateUserDTO {
    @Str({minLength: 2, maxLength: 50})
    name!: string

    @Num({minimum: 0, maximum: 150})
    age!: number

    @Bool()
    active!: boolean

    @Required()
    email!: string

    @Nullable()
    nickname!: string | null

    @Enum('admin', 'user', 'guest')
    role!: string
}

Nested Objects


@DTO()
class AddressDTO {
    @Str() street!: string
    @Str() city!: string
}

@DTO()
class UserDTO {
    @Obj(AddressDTO)
    address!: AddressDTO
}

Arrays


@DTO()
class OrderDTO {
    @Arr({type: 'string'})
    tags!: string[]

    @Arr({items: {type: 'number'}, minItems: 1, uniqueItems: true})
    itemIds!: number[]
}

Property Validation (@Expect)

Validates component properties at registration time.


@Controller('users')
class UserController {
    @Expect(CreateUserDTO)
    body!: any

    @Expect(CreateUserDTO, {required: false})
    optionalBody!: any

    @Expect(CreateUserDTO, {nullable: true})
    nullableBody!: any | null
}

Shorthand:

@Expect.Optional(CreateUserDTO)   // → { required: false }
@Expect.Nullable(CreateUserDTO)   // → { nullable: true }

Parameter Validation (@Verify)

Validates method arguments at invocation time.


@Controller('math')
class MathController {
    @Action('square')
    square(@Verify({type: 'number', minimum: 0}) n: number) {
        return n * n
    }
}

Composable Schemas

Reuse schema definitions across @Expect, @Verify, and @Obj:

const NumberSchema = {type: 'number', minimum: 0} as const
const StringSchema = {type: 'string', minLength: 1} as const

@DTO()
class ItemDTO {
    @Obj(NumberSchema)
    price!: number

    @Obj(StringSchema)
    label!: string
}

Plugins

Plugins hook into the application lifecycle.

const loggingPlugin: Plugin = {
    name: 'logger',
    onCreate(app) {
        console.log('[roost] app created')
    },
    onStaticInjected(app) {
        console.log('[roost] static injection done')
    },
    onReady(app) {
        console.log('[roost] app ready')
    },
    onError(app, err) {
        console.error('[roost] error:', err.message)
    }
}

const app = await Roost.create({
    plugins: [loggingPlugin]
})

Lifecycle order:

onCreate → register components → onStaticInjected → await registration → onReady

Errors during registration trigger onError before propagating.


Container (IoC)

The container stores singleton component instances. Components are lazily instantiated on first access.

// Register
await app.register.registerComponent(MyService)
await app.register.registerComponent('cache', RedisCache)

// Retrieve
const service = await app.container.get(MyService)
const cache = await app.container.get('cache')

Complete Example

import {
    Roost,
    Controller,
    Action,
    Module,
    Inject,
    Config,
    Initialize,
    Params,
    DTO,
    Str,
    Required,
    Verify
} from '@canlooks/roost'

// ── DTO ──────────────────────────────────────────
@DTO()
class LoginDTO {
    @Str({minLength: 3})
    username!: string

    @Str({minLength: 6})
    @Required()
    password!: string
}

// ── Services ─────────────────────────────────────
class AuthService {
    async login(username: string, password: string) {
        return {token: 'jwt-token', user: username}
    }
}

// ── Controller ───────────────────────────────────
@Controller('auth')
class AuthController {
    @Inject(AuthService)
    auth!: AuthService

    @Action('login')
    async login(
        @Verify(LoginDTO) credentials: any,
        @Params() params: Params,
    ) {
        return this.auth.login(credentials.username, credentials.password)
    }
}

// ── Plugin ───────────────────────────────────────
const perfPlugin: Plugin = {
    name: 'perf',
    onReady() {
        console.log('ready in', performance.now().toFixed(0), 'ms')
    }
}

// ── Bootstrap ────────────────────────────────────
const app = await Roost.create({
    anonymous: [AuthService, AuthController],
    plugins: [perfPlugin]
})

const [result] = await app.invoke('/auth/login', {
    username: 'alice',
    password: 'secret123'
})
console.log(result) // { token: 'jwt-token', user: 'alice' }

API Reference

Roost

| Member | Description | |--------------------------------|---------------------------| | static create(options) | Bootstrap the application | | container: Container | IoC container | | register: Register | Component registration | | invoker: Invoker | Route invocation | | invoke: Invoker.invoke | Route invocation | | pluginManager: PluginManager | Plugin event emitter | | dto: DTOManager | DTO validation manager | | pathMap | Registered path routes | | patternMap | Registered pattern routes | | regularMap | Registered regex routes |

CreateOptions

| Property | Type | Description | |--------------|---------------------------------|----------------------------| | named | Record<string, ComponentType> | Named components | | anonymous | ComponentType[] | Auto-registered components | | plugins | Plugin[] | Lifecycle plugins | | dtoOptions | AjvOptions | Options passed to ajv |

Decorators

| Decorator | Target | Description | |-------------------------------------|-----------|------------------------------------| | @Controller(path \| pattern) | Class | Declare a route controller | | @Action(path \| pattern \| regex) | Method | Declare an action handler | | @Module(components) | Class | Declare sub-component dependencies | | @Inject(target) | Property | Inject a dependency | | @Config(config) | Class | Inject default property values | | @Initialize() | Method | Run after dependencies injected | | @Params() | Parameter | Inject path parameters | | @Option() | Parameter | Inject action option | | @DTO(options?) | Class | Declare a DTO schema class | | @Obj(schema) | Property | Object/nested schema type | | @Num(options?) | Property | Number type | | @Int(options?) | Property | Integer type | | @Str(options?) | Property | String type | | @Bool() | Property | Boolean type | | @Arr(items \| options) | Property | Array type | | @Required() | Property | Mark field as required | | @Nullable() | Property | Allow null values | | @Enum(...values) | Property | Enum constraint | | @Const(value) | Property | Constant value constraint | | @Default(value) | Property | Default value | | @Expect(dto, options?) | Property | Validate property against DTO | | @Verify(dto, options?) | Parameter | Validate argument against DTO |

Plugin

interface Plugin {
    name: string
    onCreate?(app: Roost): any
    onStaticInjected?(app: Roost): any
    onReady?(app: Roost): any
    onError?(app: Roost, err: any): any
}

License

MIT