Controllers
Controllers define HTTP endpoints in the backend runtime.
They are automatically discovered, compiled and registered by OpenSya from the server/controllers directory.
Creating a Controller
A controller is created using the global defineController helper.
export default defineController(
async ({ req }) => {
const config = await useService('config.get')();
return config;
},
{ public: true },
);
The first argument is the controller handler.
The second argument defines route and access options.
Controller Context
Each controller receives a context object.
type ControllerContext = {
req: Request;
res: Response;
next: NextFunction;
};
You can access the current request, response and next function directly from the handler.
export default defineController(async ({ req }) => {
return {
user: req._user,
};
});
File-based Routing
OpenSya infers routes from the controller file path.
server/controllers/
├── candidates/
│ ├── index.get.ts
│ ├── index.post.ts
│ └── [id]/
│ └── index.get.ts
| File | Generated Route |
|---|---|
candidates/index.get.ts | GET /candidates |
candidates/index.post.ts | POST /candidates |
candidates/[id]/index.get.ts | GET /candidates/:id |
Nested Routes
Controllers can be deeply nested to create hierarchical routes.
Example:
server/controllers/
└── users/
└── [id]/
├── profile/
│ └── index.get.ts
└── jobs/
└── list.get.ts
Generated routes:
| File | Generated Route |
|---|---|
users/[id]/profile/index.get.ts | GET /users/:id/profile |
users/[id]/jobs/list.get.ts | GET /users/:id/jobs/list |
Nested routing allows applications to remain modular and organized while scaling to large infrastructures.
HTTP Method
By default, the HTTP method can be inferred from the filename.
.get.ts → GET
.post.ts → POST
.put.ts → PUT
.patch.ts → PATCH
.delete.ts → DELETE
You can also override the method manually using the method option.
export default defineController(
async ({ req }) => {
return useService('candidate.search')(req.query);
},
{
method: 'post',
path: '/candidates/search',
},
);
Custom Path
The route path can be customized using the path option.
export default defineController(
async () => {
return useService('config.get')();
},
{
public: true,
method: 'get',
path: '/system/config',
},
);
The path option can be a single path or multiple paths.
export default defineController(
async () => {
return { status: 'ok' };
},
{
public: true,
path: ['/health', '/status'],
},
);
Path Resolution
Custom controller paths can be absolute or relative.
Absolute Path
If the path starts with /, it is attached directly to the API root.
{
path: '/system/config'
}
Generated route:
/system/config
Relative Path
If the path does not start with /, it is resolved relative to the controller parent path.
Example:
server/controllers/users/[id]/profile/
{
path: 'settings'
}
Generated route:
/users/:id/profile/settings
Public Controllers
Use { public: true } to expose a controller without authentication or access checks.
export default defineController(
async () => {
return useService('config.get')();
},
{ public: true },
);
public: true for endpoints that are intentionally exposed.Protected Controllers
By default, controllers are not public.
You can define access rules using access, roles or disallowSelfAction.
export default defineController(
async ({ req }) => {
return useService('candidate.delete')(req.params.id);
},
{
roles: ['admin'],
access: true,
},
);
Access Options
The access option can be enabled with true.
{
access: true
}
It can also receive a job identifier.
{
access: {
jobID: 'job-id',
roles: ['recruiter', 'admin'],
}
}
The jobID can also be resolved from the request.
export default defineController(
async ({ req }) => {
return useService('application.list')(req.params.jobID);
},
{
access: {
jobID: (req) => req.params.jobID,
roles: ['recruiter', 'admin'],
},
},
);
Disallow Self Action
Some endpoints should prevent users from acting on themselves.
The disallowSelfAction option can be used for this kind of rule.
export default defineController(
async ({ req }) => {
return useService('user.delete')(req.params.id);
},
{
roles: ['admin'],
disallowSelfAction: (req) => req.params.id,
},
);
Controller Options
Controllers support the following options:
| Option | Type | Description |
|---|---|---|
name | string | Custom controller name. |
path | string | string[] | Custom route path or list of paths. |
method | HttpMethod | HTTP method used by the route. |
public | true | false | Expose the endpoint without authentication. |
access | AccessOptions | Define access control rules. |
roles | string[] | Restrict the controller to specific roles. |
disallowSelfAction | true | string | function | Prevent users from acting on themselves. |
Using Services
Controllers should stay thin and delegate business logic to services.
export default defineController(async ({ req }) => {
return useService('candidate.findById')(req.params.id);
});
This keeps controllers focused on HTTP concerns while services handle application logic.
Runtime Integration
During startup, OpenSya automatically:
- Discovers controller files
- Infers routes and HTTP methods
- Applies controller options
- Registers routes in the generated NestJS runtime
- Connects services, guards and access rules
Filesystem Discovery
↓
Controller Compilation
↓
Route Registration
↓
Access Resolution
↓
Runtime Execution
Philosophy
Controllers are the HTTP entry points of the OpenSya backend runtime.
They should remain small, predictable and focused on routing concerns.
Business logic should usually live in services, while controllers orchestrate request handling and access configuration.