Step 5 of 12 42% complete

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.

my-noego-app
src/server/controller/todos.controller.ts NEW
src
server
controller
openapi
todos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { Component, Inject, LoadAs } from "@noego/ioc";
import type { Request, Response } from "express";
import TodosService from "../services/todos_service";

@Component({ scope: LoadAs.Singleton })
export default class TodosController {
  constructor(
    @Inject(TodosService) private service: TodosService
  ) {}

  async getTodos({ res }: { req: Request; res: Response }) {
    const todos = await this.service.getTodos();
    return res.json({ todos });
  }

  async getTodoById({ req, res }: { req: Request; res: Response }) {
    const id = Number(req.params.id);
    const todo = await this.service.getTodoById(id);
    if (!todo) {
      return res.status(404).json({ error: "Todo not found" });
    }
    return res.json(todo);
  }

  async createTodo({ req, res }: { req: Request; res: Response }) {
    const { title, description } = req.body as { title?: string; description?: string };
    const id = await this.service.addTodo(title ?? "", description);
    return res.status(201).json({ id });
  }

  async updateTodo({ req, res }: { req: Request; res: Response }) {
    const id = Number(req.params.id);
    const changes = req.body as { title?: string; description?: string | null; completed?: boolean | number };
    try {
      await this.service.updateTodo(id, changes);
      return res.status(204).send();
    } catch (error) {
      return res.status(404).json({ error: "Todo not found" });
    }
  }

  async deleteTodo({ req, res }: { req: Request; res: Response }) {
    const id = Number(req.params.id);
    try {
      await this.service.deleteTodo(id);
      return res.status(204).send();
    } catch (error) {
      return res.status(404).json({ error: "Todo not found" });
    }
  }
}

Creating the Controller

Create a controller that injects the service and handles HTTP requests:

src/server/controller/todos.controller.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { Component, Inject, LoadAs } from "@noego/ioc";
import type { Request, Response } from "express";
import TodosService from "../services/todos_service";

@Component({ scope: LoadAs.Singleton })
export default class TodosController {
  constructor(
    @Inject(TodosService) private service: TodosService
  ) {}

  async getTodos({ res }: { req: Request; res: Response }) {
    const todos = await this.service.getTodos();
    return res.json({ todos });
  }

  async getTodoById({ req, res }: { req: Request; res: Response }) {
    const id = Number(req.params.id);
    const todo = await this.service.getTodoById(id);
    if (!todo) {
      return res.status(404).json({ error: "Todo not found" });
    }
    return res.json(todo);
  }

  async createTodo({ req, res }: { req: Request; res: Response }) {
    const { title, description } = req.body as { title?: string; description?: string };
    const id = await this.service.addTodo(title ?? "", description);
    return res.status(201).json({ id });
  }

  async updateTodo({ req, res }: { req: Request; res: Response }) {
    const id = Number(req.params.id);
    const changes = req.body as { title?: string; description?: string | null; completed?: boolean | number };
    try {
      await this.service.updateTodo(id, changes);
      return res.status(204).send();
    } catch (error) {
      return res.status(404).json({ error: "Todo not found" });
    }
  }

  async deleteTodo({ req, res }: { req: Request; res: Response }) {
    const id = Number(req.params.id);
    try {
      await this.service.deleteTodo(id);
      return res.status(204).send();
    } catch (error) {
      return res.status(404).json({ error: "Todo not found" });
    }
  }
}

Understanding Request Arguments

NoEgo passes Express request and response objects to controller methods:

  • req.body - Parsed request body (for POST, PUT, PATCH)
  • req.params - Path parameters (e.g., /todos/:id)
  • req.query - Query string parameters
  • 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 x-action specifies the method name:

src/server/openapi/todos/todos.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
module:
  todos-api:
    basePath: /api/todos
    paths:
      /:
        get:
          summary: Get all todos
          x-controller: todos.controller
          x-action: getTodos
          responses:
            '200':
              description: List of todos
        post:
          summary: Create a new todo
          x-controller: todos.controller
          x-action: createTodo
          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
          x-controller: todos.controller
          x-action: getTodoById
          responses:
            '200':
              description: Todo found
            '404':
              description: Todo not found
        patch:
          summary: Update a todo
          x-controller: todos.controller
          x-action: updateTodo
          requestBody:
            required: true
            content:
              application/json:
                schema:
                  type: object
                  additionalProperties: false
                  properties:
                    title:
                      type: string
                      minLength: 1
                      maxLength: 200
                    description:
                      type: string
                      nullable: true
                    completed:
                      type: boolean
          responses:
            '204':
              description: Todo updated
            '404':
              description: Todo not found
        delete:
          summary: Delete a todo
          x-controller: todos.controller
          x-action: deleteTodo
          responses:
            '204':
              description: Todo deleted
            '404':
              description: Todo not found

How 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: todos.controllersrc/server/controller/todos.controller.ts
  • The x-action value maps 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.

JSON Schema validation in requestBody
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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    # optional

With 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:

terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
# 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/1

Controller Best Practices

  • Keep controllers thin: Delegate business logic to services
  • Handle errors gracefully: Return appropriate status codes
  • Use x-action 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.

Troubleshooting

NoEgo

© 2025 NoEgo. All rights reserved.