This guide will help you get started with the DI Framework - a lightweight, TypeScript-first dependency injection framework with built-in HTTP server support using modern TypeScript 5.0+ decorators.
- Node.js 18+ or later
- TypeScript 5.0+
- Basic understanding of TypeScript decorators
# Clone or add to your project
pnpm install
# Build the framework
pnpm buildLet's create a simple REST API step by step.
Services contain your business logic and are decorated with @Injectable():
import { Injectable } from './src';
interface User {
id: number;
name: string;
email: string;
}
@Injectable()
class UserService {
private users: User[] = [];
private currentId = 1;
findAll(): User[] {
return this.users;
}
create(name: string, email: string): User {
const user = { id: this.currentId++, name, email };
this.users.push(user);
return user;
}
}Controllers handle HTTP requests and are decorated with @Controller():
Note: @Controller automatically marks the class as injectable - no need for @Injectable().
import {
Controller,
Get,
Post,
HttpRequest,
HttpResponse,
registerDependencies
} from './src';
@Controller('/users')
class UserController {
constructor(private readonly userService: UserService) {}
@Get()
async getAllUsers(req: HttpRequest, res: HttpResponse): Promise<void> {
const users = this.userService.findAll();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(users));
}
@Post()
async createUser(req: HttpRequest, res: HttpResponse): Promise<void> {
const { name, email } = req.body;
const user = this.userService.create(name, email);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(user));
}
}
// Register constructor dependencies (required for DI)
registerDependencies(UserController, [UserService]);Modules organize your application by grouping related components:
import { Module } from './src';
@Module({
controllers: [UserController],
providers: [UserService],
})
class AppModule {}Create the application and start the server:
import { ApplicationFactory } from './src';
async function bootstrap() {
const app = ApplicationFactory.create(AppModule);
await app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
}
bootstrap().catch(console.error);# Get all users
curl http://localhost:3000/users
# Create a user
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com"}'Important: When using route decorators (@Get, @Post, etc.), you must declare the proper function signature:
// ✅ Correct - explicit types
@Get('/users')
async getUsers(
request: HttpRequest,
response: HttpResponse
): Promise<void> {
// implementation
}
// ❌ Incorrect - missing types
@Get('/users')
async getUsers(request, response) {
// This won't work with new TypeScript decorators
}The required signature is:
(request: HttpRequest, response: HttpResponse) => void | Promise<void>Guards control access to routes. They implement the Guard interface:
Note: @GuardDecorator() automatically marks the class as injectable - no need for @Injectable().
import {
Guard,
GuardDecorator,
ExecutionContext
} from './src';
@GuardDecorator()
class AuthGuard implements Guard {
canActivate(context: ExecutionContext): boolean {
const request = context.getRequest();
const token = request.headers.authorization;
return !!token; // Simple check for demo
}
}import { UseGuards } from './src';
@Controller('/admin')
class AdminController {
// Apply to single route
@UseGuards(AuthGuard)
@Get('/dashboard')
async dashboard(req: HttpRequest, res: HttpResponse): Promise<void> {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Admin dashboard' }));
}
}
// Don't forget to register the guard in your module
@Module({
controllers: [AdminController],
providers: [AuthGuard],
})
class AppModule {}Guards are executed in order. All must pass:
@UseGuards(AuthGuard, RoleGuard, PermissionGuard)
@Get('/super-admin')
async superAdmin(req: HttpRequest, res: HttpResponse): Promise<void> {
// Only accessible if all three guards pass
}@Post('/users')
async createUser(req: HttpRequest, res: HttpResponse): Promise<void> {
// Body (automatically parsed JSON)
const data = req.body;
// Query parameters (?name=john&age=30)
const name = req.query?.name;
// Route parameters (/users/:id)
const id = req.params?.id;
// Headers
const contentType = req.headers['content-type'];
}@Get('/users/:id')
async getUser(req: HttpRequest, res: HttpResponse): Promise<void> {
const id = parseInt(req.params?.id || '0');
const user = this.userService.findById(id);
if (!user) {
// 404 Not Found
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
// 200 OK
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(user));
}Use :param syntax for dynamic route segments:
@Get('/users/:userId/posts/:postId')
async getUserPost(req: HttpRequest, res: HttpResponse): Promise<void> {
const userId = req.params?.userId;
const postId = req.params?.postId;
// Fetch and return post
}The framework uses constructor-based dependency injection:
@Injectable()
class OrderService {
constructor(
private readonly userService: UserService,
private readonly paymentService: PaymentService
) {}
}
// IMPORTANT: Register dependencies manually
registerDependencies(OrderService, [UserService, PaymentService]);The new TypeScript decorator API doesn't support emitDecoratorMetadata, which means we can't automatically infer constructor parameter types. You must manually register dependencies using registerDependencies().
All services are singletons by default - only one instance is created and shared across the application.
@Module({
controllers: [UserController, PostController],
providers: [UserService, PostService, DatabaseService],
})
class UserModule {}@Module({
controllers: [AdminController],
providers: [AdminService],
imports: [UserModule, AuthModule],
})
class AdminModule {}@Module({
providers: [ConfigService],
exports: [ConfigService], // Available to importing modules
})
class ConfigModule {}@Get('/users/:id')
async getUser(req: HttpRequest, res: HttpResponse): Promise<void> {
try {
const id = parseInt(req.params?.id || '0');
if (isNaN(id)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid ID format' }));
return;
}
const user = await this.userService.findById(id);
if (!user) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'User not found' }));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(user));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal server error' }));
}
}// ✅ Good - one resource per controller
@Controller('/users')
class UserController { }
@Controller('/posts')
class PostController { }
// ❌ Avoid mixing resources in one controller
@Controller('/api')
class ApiController {
@Get('/users') // Bad - mixed resources
@Get('/posts')
}// ✅ Good - delegate to service
@Controller('/users')
class UserController {
constructor(private userService: UserService) {}
@Post()
async create(req: HttpRequest, res: HttpResponse): Promise<void> {
const user = await this.userService.create(req.body);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(user));
}
}
// ❌ Avoid business logic in controllers
@Controller('/users')
class UserController {
@Post()
async create(req: HttpRequest, res: HttpResponse): Promise<void> {
// Validation, database access, etc. all in controller
const isValid = validateEmail(req.body.email);
const user = await database.insert(...);
await emailService.send(...);
}
}// ✅ Good - services need @Injectable()
@Injectable()
class UserService { }
// ✅ Good - controllers are auto-injectable
@Controller('/users')
class UserController { }
// ✅ Good - guards are auto-injectable
@GuardDecorator()
class AuthGuard { }
// ❌ Missing @Injectable() on service will cause DI errors
class UserService { } // Error!// ✅ Good - dependencies registered
@Controller('/users')
class UserController {
constructor(private userService: UserService) {}
}
registerDependencies(UserController, [UserService]);
// ❌ Forgot to register - will fail at runtime
@Controller('/users')
class UserController {
constructor(private userService: UserService) {}
}
// Missing: registerDependencies(UserController, [UserService]);// ✅ Use appropriate status codes
res.writeHead(200, ...); // OK
res.writeHead(201, ...); // Created
res.writeHead(400, ...); // Bad Request
res.writeHead(401, ...); // Unauthorized
res.writeHead(403, ...); // Forbidden
res.writeHead(404, ...); // Not Found
res.writeHead(500, ...); // Internal Server Error@Injectable()
class UserRepository {
private users: User[] = [];
findAll(): User[] {
return this.users;
}
findById(id: number): User | undefined {
return this.users.find(u => u.id === id);
}
save(user: User): void {
this.users.push(user);
}
}
@Injectable()
class UserService {
constructor(private repository: UserRepository) {}
async getUsers(): Promise<User[]> {
return this.repository.findAll();
}
}
registerDependencies(UserService, [UserRepository]);@Injectable()
class ValidationService {
validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
@Injectable()
class UserService {
constructor(private validator: ValidationService) {}
create(data: any): User {
if (!this.validator.validateEmail(data.email)) {
throw new Error('Invalid email');
}
// Create user
}
}
registerDependencies(UserService, [ValidationService]);# Run the main example
pnpm start
# Run the basic example
tsx examples/basic-example.ts- Check out
bin.tsfor a complete example with guards - See
examples/basic-example.tsfor a simple Todo app - Read the README.md for full API documentation
- Explore the
src/directory to understand the framework internals
Solution: Make sure to:
- Add
@Injectable()to services (controllers/guards are auto-injectable) - Register it in a module's
providersarray - Call
registerDependencies()if it has constructor parameters
Solution:
- Add
@Controller()to your controller class - Register the controller in a module's
controllersarray - Ensure route handler methods have proper type signatures
Solution:
- Add
@GuardDecorator()to guard class (automatically injectable) - Implement the
canActivate()method - Register guards in module's
providersarray
For issues and questions, please check the documentation or open an issue on the project repository.