hono-plus v1.1.9
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.
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago