Server

Controllers

Learn how to define HTTP controllers with the OpenSya Nest runtime.

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.

server/controllers/config/index.get.ts
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.

Controllers are discovered from the filesystem and registered automatically by the OpenSya runtime.

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.

server/controllers/profile/index.get.ts
export default defineController(async ({ req }) => {
  return {
    user: req._user,
  };
});

File-based Routing

OpenSya infers routes from the controller file path.

Example
server/controllers/
├── candidates/
│   ├── index.get.ts
│   ├── index.post.ts
│   └── [id]/
│       └── index.get.ts
FileGenerated Route
candidates/index.get.tsGET /candidates
candidates/index.post.tsPOST /candidates
candidates/[id]/index.get.tsGET /candidates/:id

Nested Routes

Controllers can be deeply nested to create hierarchical routes.

Example:

Nested Routes
server/controllers/
└── users/
    └── [id]/
        ├── profile/
        │   └── index.get.ts
        └── jobs/
            └── list.get.ts

Generated routes:

FileGenerated Route
users/[id]/profile/index.get.tsGET /users/:id/profile
users/[id]/jobs/list.get.tsGET /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.

Methods
.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.

server/controllers/candidates/search.ts
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.

server/controllers/system/config.ts
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.

server/controllers/health/index.ts
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
Relative paths help keep nested controllers modular and composable.

Public Controllers

Use { public: true } to expose a controller without authentication or access checks.

server/controllers/config/index.get.ts
export default defineController(
  async () => {
    return useService('config.get')();
  },
  { public: true },
);
Public controllers are accessible without authentication.Only use 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.

server/controllers/candidates/[id]/index.delete.ts
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.

server/controllers/jobs/[jobID]/applications/index.get.ts
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.

server/controllers/users/[id]/delete.ts
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:

OptionTypeDescription
namestringCustom controller name.
pathstring | string[]Custom route path or list of paths.
methodHttpMethodHTTP method used by the route.
publictrue | falseExpose the endpoint without authentication.
accessAccessOptionsDefine access control rules.
rolesstring[]Restrict the controller to specific roles.
disallowSelfActiontrue | string | functionPrevent users from acting on themselves.

Using Services

Controllers should stay thin and delegate business logic to services.

server/controllers/candidates/[id]/index.get.ts
export default defineController(async ({ req }) => {
  return useService('candidate.findById')(req.params.id);
});

This keeps controllers focused on HTTP concerns while services handle application logic.

A good controller should mainly handle request input, access rules and response output.

Runtime Integration

During startup, OpenSya automatically:

  1. Discovers controller files
  2. Infers routes and HTTP methods
  3. Applies controller options
  4. Registers routes in the generated NestJS runtime
  5. Connects services, guards and access rules
Runtime Lifecycle
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.