---
name: backend-controller
description: >
  Generates the **integration-strata** API controller (generic CRUD served at
  /api/{module}/{section} from its [NavRoute], Swagger group "integration") with
  RequirePermission, auto-mapped DTO → Command conversions, and module-scoped
  permission classes. Consumes NuGet packages (SmartStack.Core, SmartStack.Api,
  MediatR). The companion **screen-driven strata** lives in
  /api/screens/... and is generated by `scaffold-screen-controller`.
phase: development/backend
cli: cli/scaffold-controller
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# Integration Controller — API Layer (generic strata)

Generates the **integration-strata** REST controller for an entity. This is the
controller machine-to-machine consumers talk to: imports/exports, ETL, BI,
third-party integrations, batch jobs, Postman/Swagger exploration, dev tooling.

> **Two strata, one business layer.** SmartStack splits the public surface in two:
>
> | Strata | Generator | Route | Swagger group | Consumer |
> |--------|-----------|-------|---------------|----------|
> | **Integration** | `scaffold-controller` *(this skill)* | `/api/{module}/{section}` (resolved from `[NavRoute]`) | `integration` | Machine-to-machine (ETL, BI, batch, scripts) |
> | **Screens** | `scaffold-screen-controller` *(Phase 2b — fan-out per section)* | `/api/screens/{plural}/{action}` | `screens` | The generated React app — one endpoint per screen, payload shaped from the pagespec |
>
> Both strata call the **same Business layer** (`I{Entity}Service`). Business
> rules never duplicate — only the DTO shape + URL contract differ.

## When to Use

- Phase 2a (API integration) of `/ba-develop`
- When adding a generic CRUD endpoint over an entity for non-UI consumers
- Custom actions / state transitions (archive, validate, …) coming from BA UCs
  are still emitted here — the screens stratum calls them via the same Business
  method, never reimplements them

## Controller Pattern (output)

```csharp
/// <summary>
/// Integration controller — exposes generic CRUD over the entity for
/// machine-to-machine consumers. The platform serves it at /api/hrm/employees
/// (resolved from [NavRoute]); the screen-driven API lives under /api/screens/...
/// and is generated by scaffold-screen-controller.
/// </summary>
[ApiController]
[ApiExplorerSettings(GroupName = "integration")]
// No [Route]: the platform's NavigationRouteModelProvider rewrites the route from
// [NavRoute] to /api/{module}/{section} and discards any [Route]. Keep [NavRoute] only.
[NavRoute("hrm.employees")]
[Authorize]
[Produces("application/json")]
public class EmployeesController : ControllerBase
{
    private readonly IEmployeeService _service;
    public EmployeesController(IEmployeeService service) => _service = service;

    [HttpGet]
    [RequirePermission(HrmPermissions.Employees.Read)]
    public async Task<ActionResult<PaginatedResult<EmployeeListDto>>> GetAll(
        [FromQuery] int page = 1, [FromQuery] int pageSize = 20,
        [FromQuery] string? search = null, CancellationToken ct = default)
    {
        return Ok(await _service.GetAllAsync(new GetEmployeesQuery(page, pageSize, search), ct));
    }

    /// <summary>
    /// Reference surface for FK dropdowns — dual gate, ANY semantics: the
    /// dedicated Lookup grant opens id+name pairs WITHOUT the full Read surface.
    /// </summary>
    [HttpGet("lookup")]
    [RequirePermission(HrmPermissions.Employees.Lookup, HrmPermissions.Employees.Read)]
    public async Task<ActionResult<PaginatedResult<EmployeeRefDto>>> GetLookup(
        [FromQuery] string? search = null, [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20, CancellationToken ct = default)
    {
        return Ok(await _service.GetLookupAsync(new GetEmployeesLookupQuery(search, page, pageSize), ct));
    }

    [HttpPost]
    [RequirePermission(HrmPermissions.Employees.Create)]
    public async Task<ActionResult<Guid>> Create([FromBody] CreateEmployeeDto dto, CancellationToken ct = default)
    {
        var command = new CreateEmployeeCommand(dto.FirstName, dto.LastName);
        var id = await _service.CreateAsync(command, ct);
        return CreatedAtAction(nameof(GetById), new { id }, id);
    }

    [HttpPut("{id:guid}")]
    [RequirePermission(HrmPermissions.Employees.Update)]
    public async Task<ActionResult> Update(Guid id, [FromBody] UpdateEmployeeDto dto, CancellationToken ct = default)
    {
        var command = new UpdateEmployeeCommand(id, dto.FirstName, dto.LastName);
        await _service.UpdateAsync(command, ct);
        return NoContent();
    }
}
```

## ⚠ BLOCKING — Roslyn Convention Rules

**Any generated controller violating these rules MUST fail audit and be rewritten.**

- **SS001**: Must inherit `ControllerBase` + have `[ApiController]`
- **SS002**: Single service injection (no `DbContext`, `IRepository<T>`, `ILogger` as primary deps — use the service layer)
- **SS003**: `[NavRoute]` present, format `module.section` (min 2 segments)
- **SS004**: `[RequirePermission]` on EACH public endpoint (no "forgot" exceptions). The `/lookup` endpoint is the ONLY one carrying TWO permissions — `[RequirePermission(X.Lookup, X.Read)]`, ANY semantics (SmartStack ≥ 3.62): the dedicated `lookup` grant serves FK dropdowns without opening the full Read surface or the menu.
- **SS005**: Return DTOs only — NEVER domain entities (`Employee`, `Order` etc.). Use `EmployeeListDto`, `OrderDetailDto`.
- **SS006**: `CancellationToken ct` as the LAST parameter of every endpoint
- **SS007**: `NavRoute` unique project-wide — grep before generating to ensure no collision
- **SS008**: Return type is **always** `ActionResult<T>` for responses with a body, `ActionResult` for 204 NoContent. Never raw `IActionResult`.
- **SS009**: No business logic in the controller — call `_service.XxxAsync(...)` and return
- **SS010**: `[FromBody]` / `[FromQuery]` explicit on every DTO parameter (OpenAPI clarity)
- **SS011**: `Create` endpoint returns `CreatedAtAction(nameof(GetById), new { id }, id)` — not `Ok(id)`
- **SS012**: DTO → Command mapping is **fully materialised** in generated code — no `/* map dto fields */` placeholders

## ⚠ BLOCKING — Clean Architecture layers

- Controller (Api layer) imports ONLY from: `Application` (services, DTOs, commands), `Domain` (enums, value types), `SmartStack.Api.Routing` (NavRoute), `SmartStack.Application.Common.Models` (PaginatedResult), `SmartStack.Api.Authorization`. Never from `Infrastructure`.
- Service (`Application` layer) imports from `Domain` + abstractions. Never from `Api` or `Infrastructure` directly — infrastructure implementations are injected via DI.
- Violation → CS error or layer leak flagged by audit.

## Module-scoped Permissions Class Pattern

The CLI emits one Permissions file per section, placed under an app+module-scoped
namespace (App/Module classification) so two modules sharing section names never
collide — example for `applicationCode: "hr"`, module `hrm`, section `employees`:

```csharp
// src/{Ns}.Api/Permissions/Hr/Hrm/HrmPermissions.Employees.cs
namespace {Ns}.Api.Permissions.Hr.Hrm;

public static class HrmPermissions
{
    public static class Employees
    {
        public const string Access = "hr.hrm.employees.access";
        public const string Lookup = "hr.hrm.employees.lookup";
        public const string Read = "hr.hrm.employees.read";
        public const string Create = "hr.hrm.employees.create";
        public const string Update = "hr.hrm.employees.update";
        public const string Delete = "hr.hrm.employees.delete";
    }
}
```

`Access` and `Lookup` are **structural** — emitted whatever `actions[]` says:
`access` is the platform's menu/route **visibility lock** (SmartStack ≥ 3.62 — data
actions never reveal a menu node), `lookup` gates the `/lookup` reference endpoint
(dual gate with `Read`, ANY). The remaining constants follow `actions[]`.

**Permission paths are 4-segment `{app}.{module}.{section}.{action}`** — they MUST
carry the application code. The grants scaffold-core-seed writes are 4-segment and
the platform's `PermissionMatcher` does an EXACT match: a 3-segment (app-less)
constant can never be granted → every role-based user 403s on every endpoint (only
a `*` super-admin passes). The prefix is derived from `applicationCode` by default;
audit `DEV-API-021` cross-checks every emitted constant against the seeded grants.
(Frontend page permissions stay 3-segment app-LESS by design — `hasPermission()`
strips the app segment at compare time; do not "align" the two.)

## After generation — MANDATORY

Once the CLI has finished writing the controller + permissions class, invoke
the audit skill. Catches SS001–SS012 violations, missing permissions entries,
DTO-vs-entity confusion, and cross-layer imports:

```
@.claude/skills/development/audit/SKILL.md
Audit the controller + permissions class I just generated at
src/{AppCode}.Api/Controllers/... and the service contract.
```

The audit MUST be green before declaring the generation "done".

## Invocation

```bash
npx --prefer-offline tsx skills/development/backend/controller/cli/scaffold-controller/index.ts \
  --spec '{"name":"Employee","module":"hrm","appCode":"MyApp","applicationCode":"hr","section":"employees","navRoute":"hrm.employees","fields":[{"name":"firstName","type":"string","required":true},{"name":"lastName","type":"string","required":true}],"projectPath":"/path"}'
```

**Important**: the `fields` array is required — it drives the DTO → Command mapping
generation. Without it, the CLI has no way to know which fields to pass through.

**`permissionPrefix`** — OMIT it (recommended): the CLI derives
`{applicationCode}.{module}.{section}` and every constant lands as a 4-segment
path matching the seeded grants. If you do pass it, it MUST be kebab-case
`{app}.{module}.{section}[.{resource}]` and start with `applicationCode.` —
validation rejects anything else (an app-less prefix is the historical
"every role-based user 403s" bug; see DEV-API-021).
