najm-api v0.0.9
NajmApi š
Smart, lightweight DI & decorator toolkit for modern web applications. Build enterprise-grade APIs with elegance and robust security.
š Overview
NajmApi supercharges your web development experience with decorator-driven development, dependency injection, and powerful guard systems. Create maintainable, testable APIs with minimal boilerplate while leveraging blazing-fast performance.
š¦ Installation
Using npm:
npm install najm-api hono reflect-metadata
Using yarn:
yarn add najm-api hono reflect-metadata
Using bun:
bun add najm-api hono reflect-metadata
š§ Required Setup
Configure TypeScript (tsconfig.json):
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
š Project Structure
There are two ways to organize and start your NajmApi project:
Option 1: Explicit Controller and Provider Registration
Register your controllers and providers directly in the Server configuration:
// main.ts or index.ts
import { Server } from 'najm-api';
import { UserController } from './controllers/userController';
import { UserService } from './services/userService';
await Server({
port: 3000,
controllers: [UserController],
providers: [UserService]
});
Option 2: Module-based Organization (Recommended)
For larger applications, use the module organization pattern:
// main.ts or index.ts
import { Server } from 'najm-api';
import './modules'; // Import all modules
await Server({ port: 3000 });
Organize your modules folder with an index.ts that exports all nested modules:
// modules/index.ts
export * from './users';
export * from './auth';
export * from './products';
// ... and so on for all your module folders
This structure ensures all your controllers and services are properly registered with the DI container.
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
Quick Start
import { Controller, Get, Post, Injectable, Server, Guards, HttpError } from 'najm-api';
// 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 }
};
}
}
// Start the server with explicit controller and provider registration
await Server({
port: 3000,
controllers: [UserController],
providers: [UserService]
});
// Alternatively, for module-based organization:
// import './modules';
// await Server({ port: 3000 });
The server automatically detects the runtime environment:
- Works with Bun directly
- Falls back to Node.js HTTP server
- Compatible with any Hono adapter
Module Organization
A complete application structure might look like this:
src/
āāā main.ts # Entry point
āāā modules/
ā āāā index.ts # Exports all modules
ā āāā users/
ā ā āāā index.ts # Exports all from this module
ā ā āāā userController.ts
ā ā āāā userService.ts
ā ā āāā userModel.ts
ā āāā auth/
ā ā āāā index.ts
ā ā āāā authController.ts
ā ā āāā authService.ts
ā ā āāā authGuard.ts
ā āāā products/
ā āāā index.ts
ā āāā productController.ts
ā āāā productService.ts
For each module, create an index.ts file to export all components:
// modules/users/index.ts
export * from './userController';
export * from './userService';
export * from './userModel';
The root modules/index.ts then imports and re-exports everything:
// modules/index.ts
export * from './users';
export * from './auth';
export * from './products';
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' };
}
}
Dependency Injection with Scopes
The DI system supports different instance lifecycles:
// Singleton (default) - one instance per application
@Injectable()
class ConfigService {}
// Request-scoped - new instance per request
@Injectable({ scope: Scope.REQUEST })
class RequestLogger {}
// Transient - new instance every time
@Injectable({ scope: Scope.TRANSIENT })
class UniqueIDGenerator {}
// Service shorthand
@Service()
class UserService {}
// Repository shorthand
@Repository()
class UserRepository {}
// Component shorthand
@Component()
class EmailComponent {}
All these decorators automatically register the class with the DI container.
Error Handling Simplified
// Custom error with status
throw new HttpError(400, 'Invalid input');
// Error with code
throw new HttpError(403, 'Not allowed', 'FORBIDDEN');
// Pre-defined error types
HttpError.badRequest('Invalid data');
HttpError.unauthorized('Login required');
HttpError.forbidden('Admin only');
HttpError.notFound('User not found');
HttpError.internal('Something went wrong');
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
headers, // All headers
// 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 };
}
}
Response Handling
The framework automatically handles different types of responses:
// JSON Response (Default)
return { data: { id: 1, name: 'Test' } };
// Empty Response (204 No Content)
return null; // or undefined
// Text Response
return 'Hello World'; // Content-Type: text/plain
// HTML Response
return '<h1>Hello World</h1>'; // Content-Type: text/html
// Custom Status Code
return {
status: 201,
data: { created: true }
};
// Redirect Response
return {
redirect: '/new-location',
status: 302 // Optional, defaults to 302
};
// Stream Response
return new ReadableStream({
start(controller) {
controller.enqueue('Hello');
controller.enqueue(' World');
controller.close();
}
});
Response with Headers:
@Get('/with-headers')
async withHeaders() {
return {
data: { success: true },
headers: {
'X-Custom-Header': 'value',
'Cache-Control': 'max-age=3600'
}
};
}
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 };
}
}
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);
});
š¤ Contributing
Contributions are welcome! Check back soon for contribution guidelines.
š License
This project is licensed under the MIT License.
š Acknowledgments
Special thanks to the Hono team for creating an amazing foundation for this project.