A TypeScript-first web framework built on Express.js that brings decorator-based routing and dependency injection to Node.js applications.
Codex combines the type safety and modern patterns of TypeScript with the flexibility and ecosystem of Express.js. It emphasizes clean architecture through decorators, dependency injection, and modular design—eliminating boilerplate while maintaining full Express compatibility.
- Decorator-based routing — Define routes using intuitive TypeScript decorators
- Dependency injection — Built-in DI container powered by TypeDI
- Modular architecture — Organize applications into reusable, testable modules
- Type-safe — Full TypeScript support with advanced type utilities
- Express compatible — Access the entire Express middleware ecosystem
- Auto promise handling — Automatic async/await resolution and error propagation
- Fluent API — Chainable methods for clean, readable configuration
- Installation
- Quick Start
- Core Concepts
- Decorators Reference
- API Reference
- Advanced Usage
- Type Utilities
- Examples
- Best Practices
Create a new Codex application with the scaffolding tool:
npx create-codex-app my-app
cd my-app
npm installInstall Codex and required dependencies:
npm install @codex-js/core
npm install -D @types/express @types/node typescriptConfigure TypeScript by ensuring your tsconfig.json includes:
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
"esModuleInterop": true
}
}Note: The
experimentalDecoratorsandemitDecoratorMetadataflags are required for decorator support.
The scaffolding tool creates this structure automatically:
my-app/
├── src/
│ ├── core/ # Core utilities
│ │ ├── constants/
│ │ ├── errors/
│ │ ├── middlewares/
│ │ ├── types/
│ │ └── utils/
│ ├── integrations/ # External service integrations
│ │ ├── db/
│ │ ├── redis/
│ │ ├── sockets/
│ │ └── .../
│ ├── modules/ # Feature modules
│ │ ├── users/
│ │ ├── products/
│ │ └── .../
│ │
│ ├── app.ts # Application entry point
│ │
│ └── server.ts # Server configuration
│
└── package.json
import codex, { Controller, Get, Module } from '@codex-js/core'
@Controller('/api/users')
class UserController {
@Get('/')
getAllUsers() {
return { message: 'List of users' }
}
@Get('/:id')
getUserById(req: Request) {
return { id: req.params.id, name: 'John Doe' }
}
}
@Module({
controllers: [UserController],
})
class AppModule {}
const app = codex()
app.enableJson().registerModules([AppModule]).handleNotFound()
app.listen(3000, () => {
console.log('Server running on http://localhost:3000')
})Controllers handle HTTP requests and return responses. They are classes decorated with @Controller() that contain route handler methods.
Key characteristics:
- Decorated with
@Controller(basePath, middlewares?) - Contain route handlers decorated with HTTP method decorators
- Can inject services via constructor or
@Inject()decorator - Automatically serialize returned objects to JSON
Example:
@Controller('/api/products')
class ProductController {
@Get('/')
listProducts() {
return { products: [] }
}
@Post('/')
createProduct(req: Request) {
return { created: req.body }
}
@Get('/:id')
getProduct(req: Request) {
return { id: req.params.id }
}
}Services encapsulate business logic and can be injected into controllers or other services. They promote separation of concerns and testability.
Key characteristics:
- Decorated with
@Service() - Registered automatically when included in a module's
providersarray - Singleton instances managed by the DI container
- Can inject other services
Example:
@Service()
class ProductService {
findAll() {
return [
{ id: 1, name: 'Product 1' },
{ id: 2, name: 'Product 2' },
]
}
findById(id: string) {
return { id, name: `Product ${id}` }
}
}
@Controller('/api/products')
class ProductController {
constructor(private productService: ProductService) {}
@Get('/')
listProducts() {
return this.productService.findAll()
}
@Get('/:id')
getProduct(req: Request) {
return this.productService.findById(req.params.id)
}
}Repositories handle data access logic, abstracting database operations from business logic. Use the @Repo() decorator for database-related classes.
Key characteristics:
- Decorated with
@Repo() - Focus on data persistence and retrieval
- Can be injected into services
- Promotes clean architecture and testability
Example:
@Repo()
class UserRepository {
private users = new Map<string, User>()
async findAll(): Promise<User[]> {
return Array.from(this.users.values())
}
async findById(id: string): Promise<User | undefined> {
return this.users.get(id)
}
async create(user: User): Promise<User> {
this.users.set(user.id, user)
return user
}
async update(id: string, data: Partial<User>): Promise<User | null> {
const user = this.users.get(id)
if (!user) return null
Object.assign(user, data)
return user
}
async delete(id: string): Promise<boolean> {
return this.users.delete(id)
}
}Modules organize related controllers, services, and repositories into cohesive, reusable units. They define the boundaries of your application features.
Key characteristics:
- Decorated with
@Module(options) - Group related functionality together
- Can be registered with route prefixes and middleware
- Support hierarchical organization
Example:
@Module({
controllers: [UserController, AuthController],
providers: [UserService, AuthService, UserRepository],
})
class UserModule {}
// Register module
app.registerModules([UserModule])
// Register with prefix and middleware
app.registerModules([
{
route: '/api',
middlewares: [authenticate, logger],
modules: [UserModule, ProductModule],
},
])Marks a class as a controller and defines its base route path.
Parameters:
basePath(optional): Base path for all routes in this controllermiddlewares(optional): Array of Express middleware functions to apply to all routes
Examples:
// Simple controller
@Controller('/api/users')
class UserController {}
// Controller with middleware
@Controller('/api/admin', [authenticate, adminOnly])
class AdminController {}
// Root controller
@Controller()
class HealthController {
@Get('/health')
checkHealth() {
return { status: 'ok' }
}
}Marks a class as a service that can be injected via dependency injection.
Example:
@Service()
class EmailService {
async sendEmail(to: string, subject: string, body: string) {
// Email sending logic
console.log(`Sending email to ${to}`)
}
}Marks a class as a repository for data access operations.
Example:
@Repo()
class ProductRepository {
async findAll() {
// Database query
}
async findById(id: string) {
// Database query
}
}Defines a module with its controllers and providers.
Options:
controllers: Array of controller classesproviders: Array of service and repository classes
Example:
@Module({
controllers: [UserController, ProfileController],
providers: [UserService, UserRepository, EmailService],
})
class UserModule {}All HTTP method decorators follow the same pattern:
@Method(path: string, middlewares?: RequestHandler[])Parameters:
path: Route path (can include Express route parameters like:id)middlewares(optional): Array of middleware functions for this specific route
Defines a GET route handler.
@Get('/profile')
getProfile() {
return { name: 'John Doe' }
}
@Get('/users/:id', [cacheMiddleware])
getUser(req: Request) {
return { id: req.params.id }
}Defines a POST route handler.
@Post('/login')
login(req: Request) {
const { email, password } = req.body
return { token: 'jwt-token' }
}
@Post('/users', [validateUser, authenticate])
createUser(req: Request) {
return { created: true, user: req.body }
}Defines a PUT route handler for full resource updates.
@Put('/:id')
updateUser(req: Request) {
return { id: req.params.id, ...req.body }
}Defines a PATCH route handler for partial resource updates.
@Patch('/:id')
patchUser(req: Request) {
return { id: req.params.id, updated: req.body }
}Defines a DELETE route handler.
@Delete('/:id')
deleteUser(req: Request) {
return { deleted: true, id: req.params.id }
}Injects a dependency into a class property.
Parameters:
token(optional): Service class or token to inject
Examples:
class UserController {
// Automatic injection based on type
@Inject()
private userService!: UserService
// Explicit token injection
@Inject(LoggerService)
private logger!: LoggerService
}Returns the underlying Express application instance for advanced configuration.
const app = codex()
const expressApp = app.instance()
// Use Express features directly
expressApp.set('view engine', 'ejs')Sets a global base route prefix for all registered routes.
app.baseRoute('/api/v1')
// All routes will be prefixed with /api/v1Adds Express middleware to the application.
import cors from 'cors'
import helmet from 'helmet'
app.use(cors()).use(helmet()).use(customMiddleware)Adds middleware that will be executed before route registration. Useful for global middleware that should run before all routes.
app.useBefore([requestLogger, rateLimiter])Enables JSON body parsing middleware (express.json()).
app.enableJson()
// Now req.body will parse JSON payloadsEnables URL-encoded body parsing middleware.
Options:
extended: Boolean for using querystring (false) or qs (true) library
app.enableUrlEncoded({ extended: true })
// Now req.body will parse URL-encoded payloadsServes static files from a directory.
Parameters:
dirPath: Path to the directory containing static filesroute(optional): URL path to mount the static files (default: '/')options(optional): Express static middleware options
// Serve files from 'public' directory at root
app.serveStatic('public')
// Serve files at custom route
app.serveStatic('public', '/static')
// With options
app.serveStatic('uploads', '/files', {
maxAge: '1d',
dotfiles: 'deny',
})Registers application modules. Supports simple registration or registration with route prefixes and middleware.
Simple registration:
app.registerModules([UserModule, ProductModule])Registration with configuration:
app.registerModules([
{
route: '/api',
middlewares: [authenticate, logger],
modules: [UserModule, ProductModule],
},
{
route: '/admin',
middlewares: [authenticate, adminOnly],
modules: [AdminModule],
},
])Adds a 404 Not Found handler for unmatched routes. If no handler is provided, uses a default JSON response.
// Default handler
app.handleNotFound()
// Custom handler
app.handleNotFound((req, res) => {
res.status(404).render('404', { url: req.originalUrl })
})Register routes directly on the Codex instance without controllers.
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: Date.now() })
})
app.post('/webhook', webhookMiddleware, (req, res) => {
res.json({ received: true })
})
// Available methods: get, post, put, patch, deleteCodex supports middleware at multiple levels:
Global middleware:
app.use(cors())
app.use(helmet())Module-level middleware:
app.registerModules([
{
route: '/api',
middlewares: [authenticate, rateLimit],
modules: [UserModule],
},
])Controller-level middleware:
@Controller('/api/admin', [authenticate, adminOnly])
class AdminController {
@Get('/dashboard')
getDashboard() {
return { message: 'Admin dashboard' }
}
}Route-level middleware:
@Controller('/api/posts')
class PostController {
@Get('/', [cacheMiddleware])
getPosts() {
return { posts: [] }
}
@Post('/', [validatePost, authenticate])
createPost(req: Request) {
return { created: true }
}
}Constructor injection (recommended):
@Service()
class UserService {
constructor(private userRepo: UserRepository, private logger: LoggerService) {}
async getUser(id: string) {
this.logger.log(`Fetching user ${id}`)
return this.userRepo.findById(id)
}
}Property injection:
@Service()
class UserService {
@Inject()
private userRepo!: UserRepository
@Inject(LoggerService)
private logger!: LoggerService
async getUser(id: string) {
this.logger.log(`Fetching user ${id}`)
return this.userRepo.findById(id)
}
}Codex automatically handles promises and async functions. Simply return data or throw errors—no need to manually call res.json() or next().
Automatic JSON response:
@Controller('/api/users')
class UserController {
constructor(private userService: UserService) {}
@Get('/:id')
async getUser(req: Request) {
// Codex automatically sends JSON response
const user = await this.userService.findById(req.params.id)
return user
}
@Post('/')
async createUser(req: Request) {
// Errors are automatically caught and passed to error middleware
const user = await this.userService.create(req.body)
return { success: true, user }
}
}Manual response control:
When you need full control over the response (file downloads, redirects, etc.), don't return anything:
@Get('/download/:id')
async downloadFile(req: Request, res: Response) {
const filePath = await this.fileService.getPath(req.params.id)
res.download(filePath)
// Don't return anything
}
@Get('/redirect')
redirectToHome(req: Request, res: Response) {
res.redirect('/home')
// Don't return anything
}Add global error handling middleware:
import { ErrorRequestHandler } from 'express'
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
console.error(err.stack)
res.status(err.status || 500).json({
status: 'error',
message: err.message || 'Internal server error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
})
}
app.registerModules([AppModule])
app.use(errorHandler) // Add after modulesCustom error classes:
class NotFoundError extends Error {
status = 404
constructor(message: string) {
super(message)
this.name = 'NotFoundError'
}
}
@Service()
class UserService {
async findById(id: string) {
const user = await this.userRepo.findById(id)
if (!user) {
throw new NotFoundError(`User ${id} not found`)
}
return user
}
}Extends the Express Request type with custom optional properties. Useful for adding data attached by middleware.
Type signature:
type ExtendedRequest<K extends string, T> = Request & {
[P in K]?: T
}Example:
import { Request, ExtendedRequest } from '@codex-js/core'
// Define custom request type
type AuthRequest = ExtendedRequest<'user', {
id: string
email: string
role: string
}>
// Use in route handler
@Get('/profile')
getProfile(req: AuthRequest) {
// req.user is optional (can be undefined)
const user = req.user
if (!user) {
return { error: 'Not authenticated' }
}
return { profile: user }
}Makes custom properties on an ExtendedRequest required and non-nullable. Use this when you know middleware has added the property.
Type signature:
type ProtectedRequest<ER extends ExtendedRequest<string, any>> = Request & {
[K in Exclude<keyof ER, keyof Request>]-?: NonNullable<ER[K]>
}Example:
// Define extended request
type AuthRequest = ExtendedRequest<'user', { id: string; email: string }>
// Make user property required
type RequiredAuthRequest = ProtectedRequest<AuthRequest>
// Use after authentication middleware
@Get('/dashboard', [authenticate])
getDashboard(req: RequiredAuthRequest) {
// req.user is guaranteed to exist (non-nullable)
const userId = req.user.id
return { userId }
}Practical workflow:
// Middleware that adds user to request
const authenticate: RequestHandler = async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1]
if (!token) {
return res.status(401).json({ error: 'No token provided' })
}
const user = await verifyToken(token)
(req as any).user = user
next()
}
// Types
type AuthRequest = ExtendedRequest<'user', User>
type ProtectedAuthRequest = ProtectedRequest<AuthRequest>
@Controller('/api')
class ProfileController {
// Without middleware - user is optional
@Get('/maybe-auth')
maybeAuth(req: AuthRequest) {
if (req.user) {
return { authenticated: true, user: req.user }
}
return { authenticated: false }
}
// With middleware - user is required
@Get('/protected', [authenticate])
protected(req: ProtectedAuthRequest) {
// req.user is guaranteed to exist
return { message: `Hello ${req.user.email}` }
}
}A full CRUD application with repository, service, and controller layers:
import codex, {
Controller,
Service,
Repo,
Module,
Get,
Post,
Put,
Delete,
Request,
Response,
} from '@codex-js/core'
// Models
interface User {
id: string
name: string
email: string
createdAt: Date
}
// Repository Layer
@Repo()
class UserRepository {
private users = new Map<string, User>()
async findAll(): Promise<User[]> {
return Array.from(this.users.values())
}
async findById(id: string): Promise<User | undefined> {
return this.users.get(id)
}
async findByEmail(email: string): Promise<User | undefined> {
return Array.from(this.users.values()).find((u) => u.email === email)
}
async create(user: User): Promise<User> {
this.users.set(user.id, user)
return user
}
async update(id: string, data: Partial<User>): Promise<User | null> {
const user = this.users.get(id)
if (!user) return null
Object.assign(user, data)
return user
}
async delete(id: string): Promise<boolean> {
return this.users.delete(id)
}
}
// Service Layer
@Service()
class UserService {
constructor(private userRepo: UserRepository) {}
async getAllUsers(): Promise<User[]> {
return this.userRepo.findAll()
}
async getUserById(id: string): Promise<User | null> {
const user = await this.userRepo.findById(id)
if (!user) return null
return user
}
async createUser(data: Omit<User, 'id' | 'createdAt'>): Promise<User> {
const existing = await this.userRepo.findByEmail(data.email)
if (existing) {
throw new Error('Email already exists')
}
const user: User = {
id: Date.now().toString(),
createdAt: new Date(),
...data,
}
return this.userRepo.create(user)
}
async updateUser(id: string, data: Partial<User>): Promise<User | null> {
return this.userRepo.update(id, data)
}
async deleteUser(id: string): Promise<boolean> {
return this.userRepo.delete(id)
}
}
// Controller Layer
@Controller('/api/users')
class UserController {
constructor(private userService: UserService) {}
@Get('/')
async getAll() {
const users = await this.userService.getAllUsers()
return { users, count: users.length }
}
@Get('/:id')
async getOne(req: Request) {
const user = await this.userService.getUserById(req.params.id)
if (!user) {
return { error: 'User not found' }
}
return { user }
}
@Post('/')
async create(req: Request) {
try {
const user = await this.userService.createUser(req.body)
return { success: true, user }
} catch (error: any) {
return { error: error.message }
}
}
@Put('/:id')
async update(req: Request) {
const user = await this.userService.updateUser(req.params.id, req.body)
if (!user) {
return { error: 'User not found' }
}
return { success: true, user }
}
@Delete('/:id')
async delete(req: Request) {
const deleted = await this.userService.deleteUser(req.params.id)
if (!deleted) {
return { error: 'User not found' }
}
return { success: true, deleted }
}
}
// Module
@Module({
controllers: [UserController],
providers: [UserService, UserRepository],
})
class UserModule {}
// Application
const app = codex()
app.enableJson().baseRoute('/api/v1').registerModules([UserModule]).handleNotFound()
app.listen(3000, () => {
console.log('Server running on http://localhost:3000')
})Organizing a larger application with multiple modules:
import codex from '@codex-js/core'
import cors from 'cors'
import helmet from 'helmet'
// Auth Module
@Module({
controllers: [AuthController, SessionController],
providers: [AuthService, TokenService],
})
class AuthModule {}
// User Module
@Module({
controllers: [UserController, ProfileController],
providers: [UserService, UserRepository],
})
class UserModule {}
// Product Module
@Module({
controllers: [ProductController, CategoryController],
providers: [ProductService, ProductRepository, CategoryService],
})
class ProductModule {}
// Application
const app = codex()
// Global middleware
app.use(cors()).use(helmet()).enableJson().enableUrlEncoded()
// Register modules with different configurations
app.registerModules([
// Public API - no authentication
{
route: '/api/v1',
middlewares: [rateLimiter],
modules: [ProductModule],
},
// Protected API - requires authentication
{
route: '/api/v1',
middlewares: [authenticate, rateLimiter],
modules: [UserModule],
},
// Admin API - requires admin role
{
route: '/api/v1/admin',
middlewares: [authenticate, requireAdmin],
modules: [AdminModule],
},
// Auth routes - special handling
{
route: '/auth',
modules: [AuthModule],
},
])
// Static files and 404
app.serveStatic('public', '/static').handleNotFound()
// Error handling
app.use((err, req, res, next) => {
console.error(err)
res.status(err.status || 500).json({
error: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
})
})
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`)
})import { Request, Response, NextFunction, RequestHandler } from 'express'
import { ExtendedRequest, ProtectedRequest } from '@codex-js/core'
// Custom middleware
const requestLogger: RequestHandler = (req, res, next) => {
console.log(`${req.method} ${req.path}`)
next()
}
// Authentication middleware
type AuthRequest = ExtendedRequest<'user', { id: string; role: string }>
const authenticate: RequestHandler = async (req, res, next) => {
try {
const token = req.headers.authorization?.split(' ')[1]
if (!token) {
return res.status(401).json({ error: 'Authentication required' })
}
const user = await verifyToken(token)
;(req as AuthRequest).user = user
next()
} catch (error) {
res.status(401).json({ error: 'Invalid token' })
}
}
// Authorization middleware
const requireAdmin: RequestHandler = (req, res, next) => {
const user = (req as AuthRequest).user
if (!user || user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' })
}
next()
}
// Use in controllers
type ProtectedAuthRequest = ProtectedRequest<AuthRequest>
@Controller('/api/admin', [authenticate, requireAdmin])
class AdminController {
@Get('/users')
getAllUsers(req: ProtectedAuthRequest) {
// req.user is guaranteed to exist
console.log(`Admin ${req.user.id} accessing users`)
return { users: [] }
}
}-
Separation of concerns — Keep controllers thin, move business logic to services, and isolate data access in repositories
-
Dependency injection — Use constructor injection over property injection when possible. Avoid manual instantiation
-
Feature-based organization — Group related files by feature (module) rather than by type
src/modules/users/
├── user.controller.ts
├── user.service.ts
├── user.repository.ts
├── user.types.ts
└── user.module.ts
-
Module boundaries — Each module should be self-contained with clear interfaces. Modules should depend on abstractions, not concrete implementations
-
Single responsibility — Each class should have one reason to change. Controllers handle HTTP, services handle business logic, repositories handle data
- Type your requests — Use
ExtendedRequestandProtectedRequestfor type-safe middleware data
// Define once, use everywhere
type AuthRequest = ExtendedRequest<'user', User>
type ProtectedAuthRequest = ProtectedRequest<AuthRequest>
@Get('/profile', [authenticate])
getProfile(req: ProtectedAuthRequest) {
// req.user is type-safe and required
return { user: req.user }
}- Define interfaces — Create explicit interfaces for your data models
interface User {
id: string
email: string
name: string
createdAt: Date
}
interface CreateUserDTO {
email: string
name: string
password: string
}- Use strict TypeScript — Enable strict mode in your tsconfig.json for maximum type safety
- Use custom error classes — Create specific error types for different scenarios
class ValidationError extends Error {
status = 400
constructor(message: string) {
super(message)
this.name = 'ValidationError'
}
}
class NotFoundError extends Error {
status = 404
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`)
this.name = 'NotFoundError'
}
}- Centralized error handling — Use a global error handler middleware
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
const status = err.status || 500
const message = err.message || 'Internal server error'
logger.error({
error: err.name,
message,
stack: err.stack,
path: req.path,
})
res.status(status).json({
status: 'error',
message,
...(process.env.NODE_ENV === 'development' && {
stack: err.stack,
}),
})
}
app.use(errorHandler)- Validate early — Use validation middleware at the route level
const validateUser = (req: Request, res: Response, next: NextFunction) => {
const { email, name } = req.body
if (!email || !email.includes('@')) {
return next(new ValidationError('Invalid email'))
}
if (!name || name.length < 2) {
return next(new ValidationError('Name must be at least 2 characters'))
}
next()
}
@Post('/', [validateUser])
createUser(req: Request) {
// Body is guaranteed to be valid
return this.userService.create(req.body)
}- Unit test services — Services should be the primary focus of unit testing
import { UserService } from './user.service'
import { UserRepository } from './user.repository'
describe('UserService', () => {
let userService: UserService
let userRepo: jest.Mocked<UserRepository>
beforeEach(() => {
userRepo = {
findById: jest.fn(),
create: jest.fn(),
// ... other methods
} as any
userService = new UserService(userRepo)
})
it('should get user by id', async () => {
const mockUser = { id: '1', name: 'John' }
userRepo.findById.mockResolvedValue(mockUser)
const result = await userService.getUserById('1')
expect(result).toEqual(mockUser)
expect(userRepo.findById).toHaveBeenCalledWith('1')
})
})- Integration test controllers — Test the full HTTP request/response cycle
import request from 'supertest'
import codex from '@codex-js/core'
import { AppModule } from './app.module'
describe('UserController', () => {
let app: any
beforeAll(() => {
app = codex().enableJson().registerModules([AppModule]).instance()
})
it('should create a user', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@example.com' })
.expect(200)
expect(response.body.user).toHaveProperty('id')
expect(response.body.user.name).toBe('John')
})
})- Mock external dependencies — Use dependency injection to swap real implementations with mocks
- Use async/await consistently — Codex handles promises automatically
@Get('/:id')
async getUser(req: Request) {
// All async operations are properly handled
const user = await this.userService.findById(req.params.id)
const posts = await this.postService.findByUser(user.id)
return { user, posts }
}- Implement caching — Add caching middleware for expensive operations
import { cacheMiddleware } from './middleware/cache'
@Get('/expensive', [cacheMiddleware({ ttl: 300 })])
async expensiveOperation() {
// Result will be cached for 5 minutes
return await this.performExpensiveCalculation()
}- Batch database operations — Minimize database round trips
@Service()
class UserService {
async getUsersWithPosts(userIds: string[]) {
// Bad: N+1 query problem
// const users = await Promise.all(
// userIds.map(id => this.userRepo.findById(id))
// )
// Good: Single query
const users = await this.userRepo.findByIds(userIds)
const posts = await this.postRepo.findByUserIds(userIds)
return users.map((user) => ({
...user,
posts: posts.filter((p) => p.userId === user.id),
}))
}
}Issue: "Decorators are not valid here"
Solution: Ensure experimentalDecorators and emitDecoratorMetadata are enabled in tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Issue: "Cannot inject service into controller"
Solution: Make sure you import reflect-metadata at the top of your entry file
// Must be first import
import codex from '@codex-js/core'Issue: "Route not found after registration"
Solution: Check the order of operations. Register modules before calling handleNotFound()
app
.registerModules([AppModule]) // First
.handleNotFound() // ThenIssue: "Middleware not executing"
Solution: Ensure middleware is added in the correct order and check for early responses
// Global middleware before modules
app.use(cors())
app.registerModules([AppModule])
// Or route-level
@Get('/', [middleware1, middleware2]) // Executes in orderIssue: "Type errors with Request object"
Solution: Use ExtendedRequest type for custom properties
// Instead of
function handler(req: Request) {
const user = (req as any).user // Bad
}
// Use
type AuthRequest = ExtendedRequest<'user', User>
function handler(req: AuthRequest) {
const user = req.user // Type-safe
}Migrating from plain Express to Codex is straightforward:
Before (Express):
import express from 'express'
const app = express()
app.use(express.json())
app.get('/api/users', async (req, res) => {
const users = await getUsersFromDB()
res.json({ users })
})
app.post('/api/users', async (req, res) => {
const user = await createUser(req.body)
res.json({ user })
})
app.listen(3000)After (Codex):
import codex, { Controller, Service, Module, Get, Post } from '@codex-js/core'
@Service()
class UserService {
async getUsers() {
return getUsersFromDB()
}
async createUser(data: any) {
return createUser(data)
}
}
@Controller('/api/users')
class UserController {
constructor(private userService: UserService) {}
@Get('/')
async getAll() {
const users = await this.userService.getUsers()
return { users }
}
@Post('/')
async create(req: Request) {
const user = await this.userService.createUser(req.body)
return { user }
}
}
@Module({
controllers: [UserController],
providers: [UserService],
})
class AppModule {}
const app = codex()
app.enableJson().registerModules([AppModule])
app.listen(3000)Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/new-feature) - Commit your changes (
git commit -m 'Add new feature') - Push to the branch (
git push origin feature/new-feature) - Open a Pull Request
MIT
Codex is built on the shoulders of giants:
- Express.js - The fast, unopinionated web framework
- TypeDI - Dependency injection container
- TypeScript - Typed superset of JavaScript
mic drop.
