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

@lunafw/common

v1.7.0

Published

Common abstractions, decorators, guards, pipes and interceptors for the Luna framework

Readme

Installation

npm install @lunafw/core @lunafw/common

LunaFactory

LunaFactory.createApplication bootstraps the DI container, registers all controllers with the given adapter(s), and returns the application object.

// main.ts
import { LunaFactory } from '@lunafw/common'
import { ExpressAdapter } from '@lunafw/platform-express'
import { AppModule } from './app.module'

const bootstrap = async () => {
  const adapter = new ExpressAdapter({ port: 3000 })
  const app = await LunaFactory.createApplication(AppModule, adapter)
  await app.start()
  console.log(`Server on :${adapter.getPort()}`)
}
bootstrap()

Multiple adapters

Run HTTP, WebSocket, and gRPC simultaneously from the same module tree:

import { ExpressAdapter } from '@lunafw/platform-express'
import { WsAdapter }      from '@lunafw/platform-ws'
import { GrpcAdapter }    from '@lunafw/platform-grpc'

const app = await LunaFactory.createApplication(AppModule, [
  new ExpressAdapter({ port: 3000 }),
  new WsAdapter({ port: 3001 }),
  new GrpcAdapter({ port: 50051, protoPath: './users.proto', packageName: 'users' }),
])
await app.start()

Every @On handler that matches a registered adapter receives requests from that protocol — a single controller method can handle both HTTP and WebSocket simultaneously.


LunaMessage

The universal message envelope. Every handler receives one, regardless of protocol:

interface LunaMessage {
  context:  string    // 'http' | 'ws' | 'grpc'
  payload:  unknown   // request body / event data / RPC input
  metadata: {
    // HTTP
    params?:  Record<string, string>
    query?:   Record<string, unknown>
    headers?: Record<string, unknown>
    // WebSocket
    event?:    string
    socketId?: string
    // gRPC
    grpcMetadata?: Record<string, unknown>
  }
}

Controllers

@Controller

Marks a class as a controller and sets the route prefix. Must also be decorated with @Injectable() so the DI container can instantiate it.

import { Injectable } from '@lunafw/core'
import { Controller } from '@lunafw/common'

@Injectable()
@Controller('users')    // routes live under /users (HTTP) or chat.* (WebSocket)
export class UsersController {}

@On

Binds a method to a protocol event and an optional path. The adapter interprets both values according to its protocol:

| Adapter | event | path | |---|---|---| | HTTP (Express) | HTTP method ('get', 'post', 'patch', 'delete') | URL path ('/', '/:id') | | WebSocket | Event name suffix | Ignored ('/' by convention) | | gRPC | RPC method name from the proto | Ignored |

import { On } from '@lunafw/common'

@On('get', '/')         // HTTP GET /users/
@On('post', '/')        // HTTP POST /users/
@On('get', '/:id')      // HTTP GET /users/:id
@On('patch', '/:id')    // HTTP PATCH /users/:id
@On('delete', '/:id')   // HTTP DELETE /users/:id

@On('send', '/')        // WebSocket → listens for event "chat.send"
@On('GetUser')          // gRPC → handles the GetUser RPC

Parameter decorators

Extract specific values from the message directly into handler parameters instead of reading from message.metadata manually.

import { Body, Headers, Message, Param, Query } from '@lunafw/common'

@Injectable()
@Controller('users')
export class UsersController {
  // @Param — URL route parameter
  @On('get', '/:id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(id)
  }

  // @Body — full payload or a specific field
  @On('post', '/')
  create(@Body() dto: CreateUserDto) {
    return this.usersService.create(dto)
  }

  // @Body with a key
  @On('patch', '/:id/name')
  rename(@Param('id') id: string, @Body('name') name: string) {
    return this.usersService.rename(id, name)
  }

  // @Query — URL query string parameter
  @On('get', '/')
  list(@Query('page') page = '1', @Query('limit') limit = '20') {
    return this.usersService.paginate(Number(page), Number(limit))
  }

  // @Headers — a specific header value
  @On('post', '/')
  createWithAuth(
    @Body() dto: CreateUserDto,
    @Headers('authorization') token: string,
  ) {
    this.authService.verify(token)
    return this.usersService.create(dto)
  }

  // @Message — the full LunaMessage
  @On('delete', '/:id')
  remove(@Param('id') id: string, @Message() message: LunaMessage) {
    console.log('protocol:', message.context)
    return this.usersService.remove(id)
  }
}

When no parameter decorators are used, the full LunaMessage is passed as the first argument (backward-compatible):

@On('get', '/:id')
findOne(message: LunaMessage) {
  const { id } = message.metadata.params as { id: string }
  return this.usersService.findOne(id)
}

Middleware pipeline

For every incoming message the pipeline runs in this order:

Guards → Pipes → Interceptors → Handler
                                        ↑
                            Exception Filters wrap the whole chain

Each stage stacks at global → controller → method level.


Guards

Guards run first and decide whether the request should reach the handler. Return false or throw to block; return true to pass.

import { Injectable } from '@lunafw/core'
import { LunaGuard, LunaMessage } from '@lunafw/common'

@Injectable()
export class AuthGuard implements LunaGuard {
  canActivate(message: LunaMessage): boolean {
    const headers = message.metadata.headers as Record<string, string>
    return !!headers['authorization']
  }
}

Apply at controller level (all methods) or method level:

import { UseGuards } from '@lunafw/common'

@UseGuards(AuthGuard)          // class resolved via DI
@Controller('users')
export class UsersController {
  @UseGuards(new RolesGuard('admin'))   // pre-built instance
  @On('delete', '/:id')
  remove(@Param('id') id: string) { ... }
}

Execution order: controller guards first, then method guards.

Parameterized guards (via instance)

When a guard needs configuration, pass an instance directly instead of a class:

export class RolesGuard implements LunaGuard {
  constructor(private readonly role: string) {}

  canActivate(message: LunaMessage): boolean {
    const headers = message.metadata.headers as Record<string, string>
    return headers['x-role'] === this.role
  }
}

// Different roles on different routes:
@UseGuards(new RolesGuard('editor'))
@On('patch', '/:id')
update(@Param('id') id: string, @Body() dto: UpdatePostDto) { ... }

@UseGuards(new RolesGuard('admin'))
@On('delete', '/:id')
remove(@Param('id') id: string) { ... }

Pipes

Pipes transform or validate the LunaMessage before it reaches the handler. They run after guards and receive the message output by the previous pipe in the chain.

import { Injectable } from '@lunafw/core'
import { LunaMessage, LunaPipe } from '@lunafw/common'

@Injectable()
export class TrimBodyPipe implements LunaPipe {
  transform(message: LunaMessage): LunaMessage {
    if (typeof message.payload === 'object' && message.payload) {
      const trimmed = Object.fromEntries(
        Object.entries(message.payload as Record<string, unknown>).map(
          ([k, v]) => [k, typeof v === 'string' ? v.trim() : v],
        ),
      )
      return { ...message, payload: trimmed }
    }
    return message
  }
}
import { UsePipes } from '@lunafw/common'

@UsePipes(TrimBodyPipe)           // applied to all methods in the controller
@Controller('users')
export class UsersController {
  @UsePipes(new ValidationPipe(CreateUserDto))  // applied to this method only
  @On('post', '/')
  create(@Body() dto: CreateUserDto) { ... }
}

Execution order: controller pipes first, then method pipes.

Built-in ValidationPipe

Luna ships a ValidationPipe in @lunafw/common that integrates with class-validator and class-transformer:

npm install class-validator class-transformer
import { IsEmail, IsString, MinLength } from 'class-validator'

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name: string

  @IsEmail()
  email: string
}
import { ValidationPipe } from '@lunafw/common'

@On('post', '/')
@UsePipes(new ValidationPipe(CreateUserDto))
create(@Body() dto: CreateUserDto) {
  return this.usersService.create(dto)
}

Invalid requests automatically return 400 Bad Request with descriptive field errors.

Zod validation

You can also implement a pipe with Zod:

import { z, ZodSchema } from 'zod'
import { BadRequestException, LunaMessage, LunaPipe } from '@lunafw/common'

class ZodPipe<T> implements LunaPipe {
  constructor(private readonly schema: ZodSchema<T>) {}

  transform(message: LunaMessage): LunaMessage {
    const result = this.schema.safeParse(message.payload)
    if (!result.success) {
      throw new BadRequestException(result.error.issues[0]?.message ?? 'Validation failed')
    }
    return { ...message, payload: result.data }
  }
}

const CreatePostSchema = z.object({ title: z.string().min(3), body: z.string() })

@On('post', '/')
@UsePipes(new ZodPipe(CreatePostSchema))
create(@Body() dto: { title: string; body: string }) { ... }

Interceptors

Interceptors wrap handler execution and can act both before and after the handler via the next() call. They run after pipes.

import { Injectable } from '@lunafw/core'
import { LunaExecutionContext, LunaInterceptor } from '@lunafw/common'

@Injectable()
export class LoggingInterceptor implements LunaInterceptor {
  async intercept(context: LunaExecutionContext, next: () => Promise<unknown>) {
    const start = Date.now()
    const result = await next()     // call the handler chain
    const ms = Date.now() - start
    console.log(`[${context.getHandler()}] ${ms}ms`)
    return result
  }
}

@Injectable()
export class CacheInterceptor implements LunaInterceptor {
  private cache = new Map<string, unknown>()

  async intercept(context: LunaExecutionContext, next: () => Promise<unknown>) {
    const message = context.getMessage()
    const key = JSON.stringify(message.metadata.params)
    if (this.cache.has(key)) return this.cache.get(key)
    const result = await next()
    this.cache.set(key, result)
    return result
  }
}
import { UseInterceptors } from '@lunafw/common'

@UseInterceptors(LoggingInterceptor)
@Controller('users')
export class UsersController {
  @UseInterceptors(CacheInterceptor)
  @On('get', '/:id')
  findOne(@Param('id') id: string) { ... }
}

Execution order: controller interceptors wrap method interceptors wrap the handler.

controller-before → method-before → handler → method-after → controller-after

Exception Filters

Filters catch exceptions thrown anywhere in the pipeline — by guards, pipes, interceptors, or the handler itself — and turn them into a response.

Use @Catch to declare which exception types the filter handles. Omit the argument for a catch-all.

import { Injectable } from '@lunafw/core'
import { Catch, LunaExceptionFilter, LunaMessage } from '@lunafw/common'

// A domain error specific to your application
export class UserNotFoundError extends Error {
  constructor(id: string) { super(`User ${id} not found`) }
}
import { NotFoundException } from '@lunafw/common'

@Catch(UserNotFoundError)
@Injectable()
export class UserNotFoundFilter implements LunaExceptionFilter<UserNotFoundError> {
  catch(exception: UserNotFoundError, _message: LunaMessage) {
    // convert the domain error into an HTTP exception
    throw new NotFoundException(exception.message)
  }
}
import { UseFilters } from '@lunafw/common'

@UseFilters(UserNotFoundFilter)   // via DI
@Controller('users')
export class UsersController {
  @UseFilters(new UserNotFoundFilter())  // or a pre-built instance
  @On('get', '/:id')
  findOne(@Param('id') id: string) {
    throw new UserNotFoundError(id)  // caught by the filter
  }
}

Filter lookup order: method filters → controller filters → global filters. First matching filter wins.

Catch-all filter

@Catch()    // no argument = catches everything
@Injectable()
export class AllExceptionsFilter implements LunaExceptionFilter {
  catch(exception: unknown, message: LunaMessage) {
    console.error('Unhandled exception on', message.context, exception)
    throw new InternalServerErrorException()
  }
}

HTTP Exceptions

All HTTP exceptions live in @lunafw/common and can be imported from there or from @lunafw/platform-express (re-exported for backward compatibility).

import {
  BadRequestException,          // 400
  UnauthorizedException,        // 401
  ForbiddenException,           // 403
  NotFoundException,            // 404
  MethodNotAllowedException,    // 405
  RequestTimeoutException,      // 408
  ConflictException,            // 409
  GoneException,                // 410
  UnprocessableEntityException, // 422
  TooManyRequestsException,     // 429
  InternalServerErrorException, // 500
  NotImplementedException,      // 501
  ServiceUnavailableException,  // 503
} from '@lunafw/common'

throw new NotFoundException('User not found')
// → HTTP 404  { statusCode: 404, message: "User not found" }

Custom HTTP exceptions

import { HttpException } from '@lunafw/common'

export class PaymentRequiredException extends HttpException {
  constructor(message = 'Payment Required') {
    super(402, message)
  }
}

Global middleware

Apply guards, pipes, interceptors, and filters to every route without decorating each controller:

import { LunaFactory } from '@lunafw/common'
import { ExpressAdapter } from '@lunafw/platform-express'

const adapter = new ExpressAdapter({ port: 3000 })
const app = await LunaFactory.createApplication(AppModule, adapter)

app
  .useGlobalGuards(new AuthGuard())
  .useGlobalPipes(new ValidationPipe())
  .useGlobalInterceptors(new LoggingInterceptor())
  .useGlobalFilters(new AllExceptionsFilter())

await app.start()

Execution order: global → controller → method for each stage.


@SetMetadata + Reflector

Attach arbitrary metadata to classes or methods and read it inside guards, interceptors, or filters.

import { SetMetadata } from '@lunafw/common'

// Create a shorthand decorator
export const Roles = (...roles: string[]) => SetMetadata('roles', roles)
@Roles('admin')
@On('delete', '/:id')
remove(@Param('id') id: string) { ... }

Read the metadata from a guard or interceptor via Reflector:

import { Injectable } from '@lunafw/core'
import { LunaExecutionContext, LunaInterceptor, Reflector } from '@lunafw/common'

@Injectable()
export class RolesInterceptor implements LunaInterceptor {
  constructor(private readonly reflector: Reflector) {}

  async intercept(context: LunaExecutionContext, next: () => Promise<unknown>) {
    const roles = this.reflector.get<string[]>('roles', context.getMessage())
    if (roles && !this.hasRole(context.getMessage(), roles)) {
      throw new ForbiddenException('Insufficient role')
    }
    return next()
  }

  private hasRole(message: LunaMessage, roles: string[]): boolean {
    const headers = message.metadata.headers as Record<string, string>
    return roles.includes(headers['x-role'])
  }
}

License

MIT