0.0.9 • Published 3 months ago

najm-api v0.0.9

Weekly downloads
-
License
MIT
Repository
-
Last release
3 months ago

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.

0.0.9

3 months ago

0.0.8

3 months ago

0.0.7

3 months ago

0.0.6

3 months ago

0.0.5

3 months ago

0.0.4

3 months ago

0.0.3

3 months ago

0.0.2

3 months ago

0.0.1

3 months ago

0.1.0

3 months ago

0.1.4

3 months ago

0.1.3

3 months ago

0.1.2

3 months ago

0.1.1

4 months ago

1.2.1

4 months ago