0.0.9 • Published 11 months ago

najm-api v0.0.9

Weekly downloads
-
License
MIT
Repository
-
Last release
11 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

11 months ago

0.0.8

11 months ago

0.0.7

11 months ago

0.0.6

11 months ago

0.0.5

11 months ago

0.0.4

11 months ago

0.0.3

11 months ago

0.0.2

11 months ago

0.0.1

11 months ago

0.1.0

11 months ago

0.1.4

11 months ago

0.1.3

11 months ago

0.1.2

11 months ago

0.1.1

11 months ago

1.2.1

11 months ago