Step 5: Controller Layer with API Endpoints
What You'll Learn
- Create REST controllers with @Component decorator
- Use @Inject to connect controllers with services
- Define API routes in OpenAPI YAML with x-controller
- Handle request body, path params, and response status
- Add JSON Schema validation for automatic request validation
What You'll Build
A TodosController with validated REST endpoints for CRUD operations.
Understanding Controllers
Controllers are the HTTP layer of your application. They receive requests, delegate to services, and return responses. In NoEgo, controllers are defined as classes with methods that map to OpenAPI operations.
Project Files Overview
Here's an overview of the controller and API route files you'll create. Click on files in the tree to explore. Files marked with NEW are the ones you'll create.
import { Component, Inject } from "@noego/ioc";
import type { Context } from "@noego/forge";
import TodosService from "../service/todos_service";
@Component()
export default class TodosController {
constructor(
@Inject(TodosService) private service: TodosService
) {}
async getTodos(ctx: Context) {
const todos = await this.service.getTodos();
return { todos };
}
async getTodoById(ctx: Context) {
const id = Number(ctx.params.id);
const todo = await this.service.getTodoById(id);
if (!todo) {
ctx.res?.status?.(404);
return { error: "Todo not found" };
}
return todo;
}
async createTodo(ctx: Context) {
const { title, description } = ctx.req.body;
const id = await this.service.addTodo(title, description);
ctx.res?.status?.(201);
return { id };
}
async deleteTodo(ctx: Context) {
const id = Number(ctx.params.id);
try {
await this.service.deleteTodo(id);
ctx.res?.status?.(204);
return;
} catch (error) {
ctx.res?.status?.(404);
return { error: "Todo not found" };
}
}
}Creating the Controller
Create a controller that injects the service and handles HTTP requests:
import { Component, Inject } from "@noego/ioc";
import type { Context } from "@noego/forge";
import TodosService from "../service/todos_service";
@Component()
export default class TodosController {
constructor(
@Inject(TodosService) private service: TodosService
) {}
async getTodos(ctx: Context) {
const todos = await this.service.getTodos();
return { todos };
}
async getTodoById(ctx: Context) {
const id = Number(ctx.params.id);
const todo = await this.service.getTodoById(id);
if (!todo) {
ctx.res?.status?.(404);
return { error: "Todo not found" };
}
return todo;
}
async createTodo(ctx: Context) {
const { title, description } = ctx.req.body;
const id = await this.service.addTodo(title, description);
ctx.res?.status?.(201);
return { id };
}
async deleteTodo(ctx: Context) {
const id = Number(ctx.params.id);
try {
await this.service.deleteTodo(id);
ctx.res?.status?.(204);
return;
} catch (error) {
ctx.res?.status?.(404);
return { error: "Todo not found" };
}
}
}Understanding the Context
The Context type from @noego/forge provides access to request and response:
ctx.req.body- Parsed request body (for POST, PUT, PATCH)ctx.params- Path parameters (e.g.,/todos/:id)ctx.query- Query string parametersctx.res?.status?.(code)- Set HTTP response status
Defining Routes in OpenAPI
Connect your controller to routes using OpenAPI YAML. The x-controller extension points to your controller file, and operationId specifies the method name:
modules:
todos-api:
basePath: /api/todos
paths:
/:
get:
summary: Get all todos
operationId: getTodos
x-controller: controller/todos_controller
responses:
'200':
description: List of todos
post:
summary: Create a new todo
operationId: createTodo
x-controller: controller/todos_controller
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- title
properties:
title:
type: string
minLength: 1
maxLength: 200
description:
type: string
responses:
'201':
description: Todo created
/{id}:
get:
summary: Get a todo by ID
operationId: getTodoById
x-controller: controller/todos_controller
responses:
'200':
description: Todo found
'404':
description: Todo not found
delete:
summary: Delete a todo
operationId: deleteTodo
x-controller: controller/todos_controller
responses:
'204':
description: Todo deleted
'404':
description: Todo not foundHow x-controller Works
The x-controller extension tells NoEgo which file contains the controller class.
The path is relative to your server directory:
x-controller: controller/todos_controller→server/controller/todos_controller.ts- The
operationIdmaps directly to a method name on the controller class - The controller must export a default class decorated with
@Component()
Automatic Request Validation
NoEgo automatically validates incoming requests against your OpenAPI schema. If validation fails, a 400 error is returned before your controller code runs.
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- title # title is required
properties:
title:
type: string
minLength: 1 # cannot be empty
maxLength: 200 # max 200 chars
description:
type: string # optionalWith this schema, requests like these would be automatically rejected:
{}→ Missing required field "title"{"title": ""}→ title is too short (minLength: 1){"title": 123}→ title must be a string
Testing Your Endpoints
Use curl or fetch to test your API endpoints:
# Get all todos
curl http://localhost:3000/api/todos
# Create a new todo
curl -X POST http://localhost:3000/api/todos \
-H "Content-Type: application/json" \
-d '{"title": "Learn NoEgo", "description": "Build an awesome app"}'
# Get a specific todo
curl http://localhost:3000/api/todos/1
# Delete a todo
curl -X DELETE http://localhost:3000/api/todos/1Controller Best Practices
- Keep controllers thin: Delegate business logic to services
- Handle errors gracefully: Return appropriate status codes
- Use operationId consistently: Match method names exactly
- Validate with schemas: Let OpenAPI handle input validation
- Return JSON objects: NoEgo automatically serializes return values
What's Next?
Now that we have controllers to handle HTTP requests, let's create the service layer to add business logic and validation.