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

hono-plus

v1.1.9

Published

πŸš€ Supercharge your Hono apps with decorators and dependency injection.

Readme

hono-plus πŸš€

Smart, lightweight DI & decorator toolkit for Hono. Build enterprise-grade APIs with elegance and robust security.

🌟 Overview

Hono-plus supercharges your Hono experience with decorator-driven development, dependency injection, and powerful guard systems. Create maintainable, testable APIs with minimal boilerplate while leveraging Hono's blazing-fast performance.

πŸ“¦ Installation

Using npm:

npm install hono-plus hono reflect-metadata

Using yarn:

yarn add hono-plus hono reflect-metadata

Using bun:

bun add hono-plus hono reflect-metadata

πŸ”§ Required Setup

Configure TypeScript (tsconfig.json):

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

Features ⚑

πŸ›‘οΈ Powerful Guards System

  • Flexible route protection with stackable guards
  • Simple async functions returning true/false
  • Built-in error handling and status codes

🎯 Smart Dependency Injection

  • Constructor-based injection with decorators
  • Hierarchical service provider system
  • Automatic instance lifecycle management

πŸ”Œ Type-Safe Controllers

  • Decorator-based routing (@Get, @Post, etc.)
  • Automatic parameter parsing and validation
  • Intuitive error handling

πŸš€ Zero-Config Response Handling

  • Automatic content negotiation
  • Smart status code inference
  • Built-in support for JSON, Text, HTML, and Streams

πŸ” Intelligent Request Parsing

  • Automatic body parsing based on Content-Type
  • Built-in support for JSON, FormData, and raw content
  • Easy access to headers, query params, and URL data

⚑ Lightweight & Fast

  • No unnecessary dependencies
  • Minimal runtime overhead
  • Built on Hono's ultra-fast foundation

Guards Made Simple

Quick Start

import { Hono } from 'hono';
import { Controller, Get, Post, Injectable, Router, Guards } from 'hono-plus';

// Simple auth guard
const authGuard = async (header) => {
  const token = header('authorization');
  if (!token) {
    return false;  // Will return 401 Unauthorized
  }
  return true;
};

// Simple role guard
const adminGuard = async (header) => {
  const role = header('x-role');
  if (role !== 'admin') {
    throw new HttpError(403, 'Admin access required');
  }
  return true;
};

@Injectable()
class UserService {
  getUsers() {
    return [
      { id: 1, name: 'John' },
      { id: 2, name: 'Jane' }
    ];
  }
}

@Controller('/users')
@Guards(authGuard)  // Apply auth to all routes
class UserController {
  constructor(private userService: UserService) {}

  @Get()
  @Guards(adminGuard)  // Stack guards - requires both auth and admin
  async getUsers() {
    return {
      data: this.userService.getUsers()
    };
  }

  @Get('/:id')
  async getUser(params) {
    const id = params.id;
    const users = this.userService.getUsers();
    const user = users.find(u => u.id === parseInt(id));
    
    if (!user) {
      throw new HttpError(404, 'User not found');
    }
    return { data: user };
  }

  @Post()
  async createUser(body, header) {
    const role = header('x-role');
    return {
      status: 201,
      data: { created: true, role }
    };
  }
}

const app = new Hono();

Router.init({
  app,
  controllers: [UserController],
  providers: [UserService]
});

export default app;

Guards Made Simple

Guards are just async functions that return true or false:

// Basic auth guard
const authGuard = async (header) => {
  return header('authorization') ? true : false;
};

// Guard with custom error
const adminGuard = async (header) => {
  if (header('x-role') !== 'admin') {
    throw new HttpError(403, 'Admin only');
  }
  return true;
};

// Guard using multiple parameters
const validateGuard = async (body, header) => {
  if (!body.name) {
    throw new HttpError(400, 'Name required');
  }
  if (!header('x-api-key')) {
    return false;  // 401 Unauthorized
  }
  return true;
};

Using guards in controllers:

@Controller('/api')
@Guards(authGuard)  // Controller-level guard
class ApiController {
  @Post('/admin')
  @Guards(adminGuard)  // Route-level guard
  async adminOnly(body) {
    return { status: 'success' };
  }
}

Available Parameters

Both guards and controllers can access these parameters by name:

async function handler(
  // URL and Route Information
  params,          // URL parameters
  query,           // Single query parameters
  queries,         // Multiple query parameters
  path,            // Request path
  url,             // Full URL
  routePath,       // Matched route path
  matchedRoutes,   // All matched routes
  routeIndex,      // Current route index
  method,          // HTTP method
  
  // Request Data
  body,            // Parsed request body
  header,          // Function to get header value
  
  // Parsing Methods
  json,            // Function to parse body as JSON
  text,            // Function to get body as text
  arrayBuffer,     // Function to get body as ArrayBuffer
  blob,            // Function to get body as Blob
  formData,        // Function to parse form data
  
  // Validation
  valid,           // Request validation function
  
  // Context
  c,               // Hono context (also available as 'ctx' or 'context')
) {
  // Use only the parameters you need
}

Examples of using different parameters:

@Controller('/api')
class ApiController {
  // Basic URL parameters
  @Get('/users/:id')
  async getUser(params) {
    return { userId: params.id };
  }

  // Query parameters
  @Get('/search')
  async search(query, queries) {
    // Single value: ?tag=node
    const tag = query.tag;
    
    // Multiple values: ?tags=node&tags=javascript
    const allTags = queries().tags;
    
    return { tag, allTags };
  }

  // Request body with validation
  @Post('/data')
  async createData(body, valid) {
    const isValid = valid((data) => {
      return data.name && data.email;
    });
    
    if (!isValid) {
      throw new HttpError(400, 'Invalid data');
    }
    
    return { created: body };
  }

  // Manual body parsing
  @Post('/upload')
  async handleUpload(formData, text) {
    // Choose how to parse the body
    if (header('content-type').includes('form')) {
      const form = await formData();
      return { file: form.get('file') };
    } else {
      const content = await text();
      return { content };
    }
  }

  // Route information
  @Get('/info')
  async getInfo(routePath, matchedRoutes, routeIndex) {
    return {
      currentRoute: routePath,
      allRoutes: matchedRoutes,
      index: routeIndex
    };
  }

  // Using Hono context directly
  @Get('/context')
  async withContext(c) {
    return c.json({ message: 'Using context' });
  }
}

Examples:

// Get specific URL parameter
@Get('/users/:id')
async getUser(params) {
  const userId = params.id;
  return { id: userId };
}

// Access query string
@Get('/search')
async search(query) {
  const term = query.q;
  return { searching: term };
}

// Check authorization
@Post('/secure')
async secure(header) {
  const token = header('authorization');
  return { authorized: !!token };
}

Simple Error Handling

// Custom error with status
throw new HttpError(400, 'Invalid input');

// Error with code
throw new HttpError(403, 'Not allowed', 'FORBIDDEN');

// In controllers/guards
try {
  // Your logic
} catch (error) {
  if (error instanceof HttpError) {
    // Framework handles the response
    throw error;
  }
  // Unexpected errors become 500
  throw new HttpError(500, 'Server error');
}

Response Handling

The framework automatically handles different types of responses:

@Controller('/api')
class ApiController {
  // JSON Response (Default)
  @Get('/json')
  async getJson() {
    return {
      data: { id: 1, name: 'Test' }
    };
  }

  // Empty Response (204 No Content)
  @Post('/empty')
  async empty() {
    return null;  // or undefined
  }

  // Text Response
  @Get('/text')
  async getText() {
    return 'Hello World';  // Content-Type: text/plain
  }

  // HTML Response
  @Get('/html')
  async getHtml() {
    return '<h1>Hello World</h1>';  // Content-Type: text/html
  }

  // Custom Status Code
  @Post('/created')
  async create() {
    return {
      status: 201,
      data: { created: true }
    };
  }

  // Redirect Response
  @Get('/redirect')
  async redirect() {
    return {
      redirect: '/new-location',
      status: 302  // Optional, defaults to 302
    };
  }

  // Error Response
  @Get('/error')
  async error() {
    throw new HttpError(400, 'Bad Request');
  }

  // Custom Error Response
  @Get('/custom-error')
  async customError() {
    throw new HttpError(403, 'Not allowed', 'FORBIDDEN');
  }

  // Structured Error
  @Get('/structured')
  async structured() {
    return {
      status: 'error',
      code: 'VALIDATION_FAILED',
      message: 'Invalid input',
      details: {
        field: 'email',
        reason: 'Invalid format'
      }
    };
  }

  // Stream Response
  @Get('/stream')
  async getStream(c) {
    const stream = new ReadableStream({
      start(controller) {
        controller.enqueue('Hello');
        controller.enqueue(' World');
        controller.close();
      }
    });
    return new Response(stream);
  }

  // File Response
  @Get('/file')
  async getFile() {
    return {
      headers: {
        'Content-Type': 'application/pdf',
        'Content-Disposition': 'attachment; filename="file.pdf"'
      },
      body: fileContent  // Your file content
    };
  }

  // Conditional Response
  @Get('/conditional')
  async conditional(header) {
    const etag = '"123"';
    if (header('if-none-match') === etag) {
      return {
        status: 304  // Not Modified
      };
    }
    return {
      data: { id: 1 },
      headers: {
        'ETag': etag
      }
    };
  }
}

Response with Headers:

@Get('/with-headers')
async withHeaders() {
  return {
    data: { success: true },
    headers: {
      'X-Custom-Header': 'value',
      'Cache-Control': 'max-age=3600'
    }
  };
}

Response Structure Options:

// Simple data response
return { data: someData };

// Full response control
return {
  status: 200,                    // HTTP status code
  data: someData,                 // Response data
  headers: {                      // Custom headers
    'X-Custom-Header': 'value'
  },
  code: 'SUCCESS',               // Optional response code
  message: 'Operation complete'  // Optional message
};

// Error response
return {
  status: 'error',
  code: 'VALIDATION_ERROR',
  message: 'Invalid input',
  details: { ... }  // Optional error details
};

Body Parsing

The framework automatically parses request bodies based on Content-Type header:

@Controller('/api')
class ApiController {
  // Automatic JSON parsing
  @Post('/json')
  async handleJson(body) {
    // Content-Type: application/json
    // Body is automatically parsed as JSON
    return { received: body };
  }

  // Form data parsing
  @Post('/form')
  async handleForm(body) {
    // Content-Type: multipart/form-data
    // or application/x-www-form-urlencoded
    // Body is automatically parsed as form data
    return { received: body };
  }

  // Text content
  @Post('/text')
  async handleText(body) {
    // Content-Type: text/plain
    // Body is received as string
    return { received: body };
  }

  // Manual parsing if needed
  @Post('/manual')
  async handleManual(json, text, formData) {
    // Choose your parser based on needs
    const jsonData = await json();
    // or
    const textData = await text();
    // or
    const formContent = await formData();
    
    return { parsed: jsonData };
  }

  // File upload handling
  @Post('/upload')
  async handleUpload(formData) {
    const form = await formData();
    const file = form.get('file');
    const name = form.get('name');
    
    return {
      fileName: file.name,
      fileType: file.type,
      name: name
    };
  }
}

Dependency Injection

Hono-plus provides a powerful dependency injection system:

// Define injectable services
@Injectable()
class AuthService {
  validateToken(token) {
    return token.startsWith('Bearer ');
  }
}

@Injectable()
class UserService {
  constructor(private authService: AuthService) {
    // Services can inject other services
  }

  async getUser(id) {
    return { id, name: 'Test' };
  }
}

// Use services in controllers
@Controller('/api')
class ApiController {
  constructor(
    private userService: UserService,
    private authService: AuthService
  ) {}

  @Get('/user/:id')
  async getUser(params) {
    return this.userService.getUser(params.id);
  }
}

// Register everything
const app = new Hono();

Router.init({
  app,
  controllers: [ApiController],
  providers: [
    UserService,
    AuthService,
    // You can also register with options:
    { 
      provide: ConfigService,
      useClass: DevConfigService 
    }
  ]
});

Example with more complex services:

@Injectable()
class LoggerService {
  log(message) {
    console.log(`[${new Date().toISOString()}] ${message}`);
  }
}

@Injectable()
class DatabaseService {
  constructor(private logger: LoggerService) {}

  async query(sql) {
    this.logger.log(`Executing: ${sql}`);
    // Database logic here
  }
}

@Injectable()
class UserRepository {
  constructor(
    private db: DatabaseService,
    private logger: LoggerService
  ) {}

  async findUser(id) {
    this.logger.log(`Finding user: ${id}`);
    return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
  }
}

@Controller('/users')
class UserController {
  constructor(
    private users: UserRepository,
    private logger: LoggerService
  ) {}

  @Get('/:id')
  async getUser(params) {
    this.logger.log(`Request for user: ${params.id}`);
    return this.users.findUser(params.id);
  }
}

// Register everything
Router.init({
  app,
  controllers: [UserController],
  providers: [
    UserRepository,
    DatabaseService,
    LoggerService
  ]
});

Testing Made Easy

// Test a guard
test('auth guard', async () => {
  const header = (name) => name === 'authorization' ? 'Bearer token' : null;
  const result = await authGuard(header);
  expect(result).toBe(true);
});

// Test a controller
test('get user', async () => {
  const params = { id: '1' };
  const result = await controller.getUser(params);
  expect(result.data.id).toBe(1);
});

🀝 Contriuting

Contributions are welcome! soon.

πŸ“ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

Special thanks to the Hono team for creating an amazing foundation for this project.