Najm Framework π
A modern, modular TypeScript framework for building scalable APIs with decorators, dependency injection, and plugin architecture.
Najm is built on Hono.js and uses diject for dependency injection. It provides a powerful decorator-driven development experience with first-class support for transactions, events, guards, i18n, and more.
β¨ Key Features
- π― Decorator-Driven - Clean, declarative API with TypeScript decorators
- π Modular Plugin System - Install only what you need
- π Powerful DI Container - Advanced dependency injection with scopes (via diject)
- π‘οΈ Guards & Security - Function and class-based route protection
- πΎ Transaction Management - Automatic transactions with retry logic
- π‘ Event System - Built-in event emitter with decorator support
- π i18n Support - Multi-language with automatic detection
- πͺ Cookie Management - Secure cookie handling
- π CORS Configuration - Flexible CORS setup
- π Built-in Logging - Structured logging with request ID tracking
- π± Database Seeding - Idempotent seeding with conflict resolution
π¦ Installation
Core Framework
bun add najm diject hono reflect-metadata
npm install najm diject hono reflect-metadata
Plugins
Auto-registered plugins (included, no need to install separately):
najm-middleware - Middleware management
najm-params - Parameter resolution
najm-router - HTTP routing
Optional plugins (install as needed):
bun add najm-guard
bun add najm-database
bun add najm-event
bun add najm-cors
bun add najm-cookies
bun add najm-i18n
bun add najm-guard najm-database najm-event najm-cors najm-cookies najm-i18n
π§ Setup
TypeScript Configuration
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2020",
"module": "ESNext"
}
}
Import reflect-metadata
import "reflect-metadata";
π Quick Start
Basic Server
import "reflect-metadata";
import { Server } from "najm";
import { Service, Controller } from "diject";
import { Get, Post } from "najm-router";
import { Body, Params } from "najm-params";
@Service()
class UserService {
getUsers() {
return [{ id: 1, name: "John" }, { id: 2, name: "Jane" }];
}
}
@Controller("/users")
class UserController {
constructor(private userService: UserService) {}
@Get("/")
getUsers() {
return this.userService.getUsers();
}
@Get("/:id")
getUser(@Params("id") id: string) {
return { id, name: "User " + id };
}
@Post("/")
createUser(@Body() data: any) {
return { created: true, ...data };
}
}
await new Server()
.load(UserController, UserService)
.log("π Server starting on port 3000")
.listen(3000);
Full-Featured Server
import "reflect-metadata";
import { Server } from "najm";
import { Service, Controller, Repository } from "diject";
import { guards } from "najm-guard";
import { database } from "najm-database";
import { events } from "najm-event";
import { cors } from "najm-cors";
import { cookies } from "najm-cookies";
import { i18n } from "najm-i18n";
await new Server()
.use(cors({ origin: "*" }))
.use(database({ default: myDb }))
.use(i18n({ translations: { en, fr } }))
.use(guards())
.use(events())
.base("/api")
.scan("./src/features")
.log("β
Plugins configured")
.log("π Starting server...")
.listen(3000);
Next.js Integration
Najm works as an API backend inside Next.js App Router using a catch-all route.
1. Configure next.config.ts:
reflect-metadata and native database drivers must be externalized from the Next.js bundle:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
serverExternalPackages: ['reflect-metadata', 'better-sqlite3'],
};
export default nextConfig;
2. Create a shared server instance:
Use .load() with barrel imports instead of .scan() β bundlers can't resolve dynamic filesystem imports.
import 'reflect-metadata';
import { Server } from 'najm';
import { database } from 'najm-database';
import * as features from './features';
export const server = new Server()
.use(database({ default: db }))
.base('/api')
.load(features);
3. Create the catch-all API route:
import { handle } from 'najm';
import { server } from '@/server';
export const GET = handle(server);
export const POST = handle(server);
export const PUT = handle(server);
export const PATCH = handle(server);
export const DELETE = handle(server);
handle() wraps server.fetch for Next.js route handlers. The server auto-initializes on the first request.
| Entry point | server.listen(3000) | handle(server) in catch-all route |
| Class discovery | .scan('./src/features') | .load(featuresModule) with barrel imports |
| Config | None | serverExternalPackages in next.config.ts |
π Core Concepts
Server API
const server = new Server(opts?)
server.use(plugin)
server.load(...classes)
server.scan(path)
server.base(path)
server.middleware(...handlers)
server.set(key, value)
server.log(...messages)
await server.listen(port, cb?)
await server.init()
const handler = server.fetch
server.isRunning
await server.stop()
Default plugins (auto-registered):
middleware() - Middleware management
params() - Parameter resolution
router() - HTTP routing
Optional plugins (must be explicitly registered):
guards(), database(), events(), cors(), cookies(), i18n(), auth(), etc.
Dependency Injection
Najm uses diject - a standalone, framework-agnostic dependency injection container with decorators, scopes (SINGLETON, REQUEST, TRANSIENT), and AsyncLocalStorage support.
import { Service, Controller, Injectable } from "diject";
@Service()
class ConfigService {
getConfig() { return { apiUrl: "..." }; }
}
@Controller("/api")
class ApiController {
constructor(private config: ConfigService) {}
}
@Injectable()
class MyComponent {}
HTTP Routing
import { Controller } from "diject";
import { Get, Post, Put, Patch, Delete } from "najm-router";
@Controller("/api/users")
class UserController {
@Get("/")
getAll() { return []; }
@Get("/:id")
getById() { return {}; }
@Post("/")
create() { return {}; }
@Put("/:id")
update() { return {}; }
@Patch("/:id")
patch() { return {}; }
@Delete("/:id")
remove() { return {}; }
}
Parameter Decorators
import { Body, Params, Query, Headers, Ctx, Cookie } from "najm-params";
@Controller("/api")
class DataController {
@Get("/users/:id")
getUser(
@Params("id") id: string,
@Query("page") page: string,
@Headers("authorization") auth: string,
@Cookie("session") session: string,
@Body() body: any,
@Ctx() ctx: any
) {
return { id, page, auth, session };
}
}
Available parameter decorators:
- Body:
@Body(), @JsonBody(), @TextBody(), @FormData()
- URL/Route:
@Params(), @Query(), @Queries(), @Path(), @Url(), @Method()
- Headers:
@Headers(), @ContentType(), @Origin(), @Referer(), @Language()
- Context:
@Ctx(), @Req(), @Cookie(), @File(), @IP()
- Guard Data:
@User(), @Owner(), @Info(), @Data(), @Filter()
Guards & Authorization
import { Service, Container, DI } from "diject";
import { Guards } from "najm-guard";
import { Headers } from "najm-params";
import { USER } from "najm-guard";
const authGuard = async (headers, cookie, context) => {
const token = headers("authorization");
if (!token) throw new Error("Unauthorized");
return { userId: "123" };
};
@Service()
class RoleGuard {
@DI() container!: Container;
async canActivate(@Headers("authorization") token: string) {
const user = await this.verifyToken(token);
this.container.set(USER, user);
return true;
}
}
@Controller("/admin")
@Guards(authGuard, RoleGuard)
class AdminController {
@Get("/users")
@Guards(adminOnlyGuard)
getUsers(@User() user: any) {
return { users: [], currentUser: user };
}
}
Database & Transactions
import { Repository, Service } from "diject";
import { DB, Transaction } from "najm-database";
@Repository("postgres")
class UserRepository {
@DB("postgres") db: any;
async findById(id: string) {
return this.db.query("SELECT * FROM users WHERE id = ?", [id]);
}
async create(data: any) {
return this.db.query("INSERT INTO users ...", [data]);
}
}
@Service()
class OrderService {
constructor(
private orderRepo: OrderRepository,
private inventoryRepo: InventoryRepository
) {}
@Transaction({ retries: 2 })
async createOrder(data: any) {
const order = await this.orderRepo.create(data);
await this.inventoryRepo.decrementStock(data.items);
return order;
}
}
Event System
import { Service } from "diject";
import { Events, On } from "najm-event";
@Service()
class UserService {
@Events() events: { emit, on, off };
async createUser(data: any) {
const user = await this.repo.create(data);
this.events.emit("user.created", { userId: user.id });
return user;
}
}
@Service()
class EmailService {
@On("user.created")
async sendWelcome({ userId }: { userId: string }) {
console.log(`Sending welcome email to user ${userId}`);
}
}
Validation & DTOs
Najm uses Zod-based DTOs (Data Transfer Objects) for request validation via the najm-validation plugin. DTOs define the expected shape and constraints of incoming data, while Validators handle async business rules.
import { z } from "zod";
export const createProductDto = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
price: z.number().positive("Price must be positive"),
category: z.string().optional(),
});
export const updateProductDto = createProductDto.partial();
export const productIdParam = z.object({
id: z.string().length(5, "Product ID must be 5 characters"),
});
export type CreateProductDto = z.infer<typeof createProductDto>;
export type UpdateProductDto = z.infer<typeof updateProductDto>;
Use @Validate() in controllers to auto-validate requests:
import { Controller, Post, Patch } from "najm-router";
import { Validate } from "najm-validation";
import { Body, Params } from "najm-params";
import { createProductDto, updateProductDto, productIdParam } from "./product.dto";
@Controller("/products")
class ProductController {
@Post("/")
@Validate(createProductDto)
create(@Body() data: CreateProductDto) {
return this.service.create(data);
}
@Patch("/:id")
@Validate({ params: productIdParam, body: updateProductDto })
update(@Params("id") id: string, @Body() data: UpdateProductDto) {
return this.service.update(id, data);
}
}
Validation flow:
@Validate(dto) - Schema validation (format, length, type) β 400 if invalid
Validator.check() - Business validation (unique, exists) β 404/409/403 if invalid
Service.execute() - Business logic
Internationalization
import { Controller, Service } from "diject";
import { I18n } from "najm-i18n";
import { Get, Query } from "najm-router";
@Controller("/api")
class ApiController {
@I18n() t: any;
@Get("/greeting")
getGreeting(@Query("name") name: string) {
return {
message: this.t("welcome", { name }),
lang: this.t.getCurrentLanguage()
};
}
}
@Service()
class NotificationService {
@I18n("errors") t: any;
sendError() {
return this.t("notFound");
}
}
Logging
Najm includes a built-in logging system with support for different log levels, formats, and request ID tracking.
import { Service } from "diject";
import { Log } from "najm";
@Service()
class UserService {
@Log() logger!: LoggerService;
async createUser(data: any) {
this.logger.info("Creating user", { email: data.email });
try {
const user = await this.repo.create(data);
this.logger.debug("User created successfully", { userId: user.id });
return user;
} catch (error) {
this.logger.error("Failed to create user", error, { email: data.email });
throw error;
}
}
}
const server = new Server()
.log("π Starting server...")
.use(database({ default: db }))
.log("β
Database configured")
.load(UserController, UserService);
await server.listen(3000);
Log Levels: DEBUG, INFO, WARN, ERROR, SILENT
Configuration:
const server = new Server({
logger: {
level: 'INFO',
format: 'pretty',
includeTimestamp: true,
includeRequestId: true,
colors: true,
}
});
Database Seeding
Najm provides a powerful seeding system via SeedService from najm-database.
import { Server } from "najm";
import { SeedService } from "najm-database";
import { authSeed } from "najm-auth";
import { database } from "najm-database";
const server = await new Server({ isolated: true })
.use(database({ default: db }))
.scan('./src/features')
.log("π± Seeding database...")
.init();
const seeder = server.container.get(SeedService);
const report = await seeder.run(
{
...authSeed({
adminEmail: 'admin@example.com',
adminPass: 'Admin123!',
roles: [
{ name: 'admin', description: 'Administrator' },
{ name: 'user', description: 'Regular user' },
],
permissions: [
{ action: 'create', resource: 'posts', name: 'create:posts' },
{ action: 'read', resource: 'posts', name: 'read:posts' },
],
}),
products: {
by: ['id'],
rows: [
{ id: '1', name: 'Product 1', price: 99.99 },
{ id: '2', name: 'Product 2', price: 149.99 },
],
},
},
{
verbose: true,
onConflict: 'skip',
transaction: false,
}
);
server
.log(`β
Seed complete`)
.log(`π Total operations: ${report.items.length}`)
.log('π Test user: admin@example.com / Admin123!');
await server.stop();
Benefits:
- Idempotent seeding (safe to run multiple times)
- Conflict resolution strategies (
skip, update)
- Dependency-aware (seed users before products that reference them)
- Built-in auth seeding with
authSeed()
ποΈ Project Structure
Packages
packages/
βββ najm/ # Public-facing framework package (this package)
βββ najm-core/ # Framework core & orchestration
βββ najm-guard/ # Authorization plugin (optional)
βββ najm-database/ # Database & transactions plugin (optional)
βββ najm-event/ # Event system plugin (optional)
βββ najm-cors/ # CORS handling plugin (optional)
βββ najm-cookies/ # Cookie management plugin (optional)
βββ najm-i18n/ # Internationalization plugin (optional)
βββ najm-auth/ # Authentication plugin (optional)
βββ najm-validation/ # Validation plugin (optional)
βββ najm-rate/ # Rate limiting plugin (optional)
βββ najm-email/ # Email service plugin (optional)
βββ najm-cache/ # Cache management plugin (optional)
Note: Dependency injection is provided by diject, an external standalone package.
Application Structure (Recommended)
Feature-based structure (recommended for scalability):
src/
βββ config/ # Plugin configurations
β βββ database.ts # Database plugin config
β βββ auth.ts # Auth plugin config
β βββ plugins.ts # Export all configs
βββ features/ # Feature modules (auto-discovered by .scan())
β βββ user/
β β βββ user.controller.ts
β β βββ user.dto.ts # Zod validation schemas + inferred types
β β βββ user.validator.ts # Business validation (uniqueness, existence)
β β βββ user.service.ts
β β βββ user.repository.ts
β β βββ index.ts
β βββ product/
β βββ product.controller.ts
β βββ product.dto.ts
β βββ product.service.ts
β βββ index.ts
βββ database/ # Centralized database schema & setup
β βββ schema.ts # All table definitions (+ authSchema if using najm-auth)
β βββ seed.ts # Seeding script
βββ listeners/ # Event listeners
β βββ user.listener.ts
βββ locales/ # i18n translations
β βββ en.ts
β βββ fr.ts
βββ main.ts # Server entry point
Example main.ts:
import 'reflect-metadata';
import { Server } from 'najm';
import {
databaseConfig,
authConfig,
corsConfig,
i18nConfig
} from './config/plugins';
import { ProductListener } from './listeners';
const PORT = Number.parseInt(process.env.PORT || '3000', 10);
await new Server()
.use(corsConfig())
.use(databaseConfig())
.use(i18nConfig())
.use(authConfig())
.base('/api')
.scan('./src/features')
.load(ProductListener)
.log('β
Server configured')
.log(`π Starting on port ${PORT}`)
.listen(PORT);
π Creating Custom Plugins
Najm provides a fluent API for creating plugins using the plugin() builder:
import { plugin, Meta, Service, Inject } from "najm";
export const MY_PLUGIN_CONFIG = Symbol("MY_PLUGIN_CONFIG");
@Service()
@Meta({ layer: "plugin", order: 20 })
class MyPluginService {
@Inject(MY_PLUGIN_CONFIG) config: any;
async scan() {
}
async configure() {
}
async activate() {
}
async onReady() {
}
}
export const myPlugin = (config?: any) =>
plugin("my-plugin")
.version("1.0.0")
.services(MyPluginService)
.config(MY_PLUGIN_CONFIG, config ?? {})
.build();
export const advancedPlugin = (config?: any) =>
plugin("advanced-plugin")
.version("2.0.0")
.depends(otherPlugin())
.requires("database")
.contributes(TOKEN, value)
.services(PluginService)
.config(PLUGIN_CONFIG, config)
.set(EXTRA_TOKEN, extraValue)
.build();
const server = new Server()
.use(myPlugin({ option: "value" }))
.load(AppController);
π Complete Example
import "reflect-metadata";
import { Server } from "najm";
import { Service, Controller, Repository, DI, Container } from "diject";
import { guards } from "najm-guard";
import { database } from "najm-database";
import { events } from "najm-event";
import { Get, Post, Body, Params, Headers } from "najm-router";
import { Guards } from "najm-guard";
import { DB, Transaction } from "najm-database";
import { Events, On } from "najm-event";
import { USER } from "najm-guard";
@Repository("postgres")
class UserRepository {
@DB("postgres") db: any;
async findById(id: string) {
return this.db.query("SELECT * FROM users WHERE id = ?", [id]);
}
async create(data: any) {
return this.db.query("INSERT INTO users VALUES (?)", [data]);
}
}
@Service()
class UserService {
constructor(private repo: UserRepository) {}
@Events() events: any;
@Transaction({ retries: 2 })
async createUser(data: any) {
const user = await this.repo.create(data);
this.events.emit("user.created", { userId: user.id });
return user;
}
}
@Service()
class AuthGuard {
@DI() container!: Container;
async canActivate(@Headers("authorization") token: string) {
const user = await this.verifyToken(token);
this.container.set(USER, user);
return true;
}
}
@Service()
class EmailService {
@On("user.created")
async sendWelcome({ userId }: any) {
console.log(`Welcome email sent to user ${userId}`);
}
}
@Controller("/api/users")
@Guards(AuthGuard)
class UserController {
constructor(private userService: UserService) {}
@Get("/:id")
async getUser(@Params("id") id: string) {
return this.userService.findById(id);
}
@Post("/")
async createUser(@Body() data: any) {
return this.userService.createUser(data);
}
}
await new Server()
.use(database({ default: postgresDb }))
.use(guards())
.use(events())
.load(UserController, UserService, UserRepository, AuthGuard, EmailService)
.log("β
All services loaded")
.log("π Server starting...")
.listen(3000);
π§ͺ Testing
import { describe, test, expect, afterEach } from "bun:test";
import { Server } from "najm";
import { Controller } from "diject";
import { Get } from "najm-router";
let server: Server;
afterEach(async () => { await server?.stop(); });
test("should handle GET request", async () => {
@Controller("/test")
class TestController {
@Get("/") get() { return { ok: true }; }
}
server = await new Server({ isolated: true })
.load(TestController)
.listen(3100);
const res = await fetch("http://localhost:3100/test");
expect(await res.json()).toEqual({ ok: true });
});
π Documentation
Getting Started
Architecture
Core Concepts
Plugins
π€ Contributing
Contributions are welcome! Please submit a Pull Request.
π License
MIT License
π Acknowledgments
Najm is built on top of excellent open-source projects:
- Hono - The ultrafast web framework for the Edges
- diject - Standalone dependency injection container (originally developed for Najm, now maintained as an independent package)
- mitt - Tiny functional event emitter