# {{ProjectName}}.Api - Memory

## Purpose

HTTP entry point. REST API. Receives requests, delegates to Application layer, returns responses.

## Dependencies

- **{{ProjectName}}.Application** (commands, queries, DTOs)
- **{{ProjectName}}.Infrastructure** (DI registration)
- **MediatR** (sending commands/queries)
- **Scalar.AspNetCore** (OpenAPI UI)

---

## Two API strata — integration vs screens

The public surface is split in two so that machine-to-machine consumers and
the React app each get the contract they actually need. Both strata call the
**same business layer** — rules and state transitions are never duplicated.

| Strata | Route prefix | Swagger group | Generated by | Consumer |
|--------|--------------|---------------|--------------|----------|
| **Integration** | `/api/v1/integration/{plural}` | `integration` | `scaffold-controller` | Imports/exports, ETL, BI, batch jobs, scripts, Postman/Scalar exploration, third-party connectors |
| **Screens** | `/api/screens/{plural}/{action}` | `screens` | `scaffold-screen-controller` (Phase 2b) | The generated React app — one endpoint per screen, payload shaped from the pagespec |

**When writing a controller by hand (between CLI generations):**

- If it serves a non-UI consumer → place it in `Controllers/{Module}/` with
  `[ApiExplorerSettings(GroupName = "integration")]` + `[Route("api/v1/integration/[controller]")]`.
- If it serves a specific screen → prefer regenerating via `scaffold-screen-controller`
  rather than writing by hand. If you must hand-write, place it in
  `Controllers/{Module}/Screens/` with `[ApiExplorerSettings(GroupName = "screens")]`
  + `[Route("api/screens/{plural}/{action}")]` and ensure it calls the same
  `I{Entity}Service` methods as the integration controller.

---

## OpenAPI / Swagger Configuration (CRITICAL)

### Package Requirements

```xml
<!-- {{ProjectName}}.Api.csproj -->
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.0.0" />
```

### Program.cs Configuration

```csharp
// Services
builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, ct) =>
    {
        document.Info = new OpenApiInfo
        {
            Title = "{{ProjectName}} API",
            Version = "v1",
            Description = "{{ProjectName}} REST API"
        };
        return Task.CompletedTask;
    });
});

// Middleware (after app.Build())
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();                    // /openapi/v1.json
    app.MapScalarApiReference(options => // /scalar/v1
    {
        options.WithTitle("{{ProjectName}} API")
               .WithTheme(ScalarTheme.BluePlanet)
               .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient);
    });
}
```

### XML Documentation (MANDATORY for all Controllers)

```xml
<!-- {{ProjectName}}.Api.csproj -->
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```

### Controller Documentation Pattern

```csharp
/// <summary>
/// Manages orders
/// </summary>
[ApiController]
[ApiExplorerSettings(GroupName = "integration")]
[Route("api/v1/integration/[controller]")]
[Produces("application/json")]
[Tags("Orders")]
public class OrdersController : ControllerBase
{
    /// <summary>
    /// Retrieves all orders with optional filtering
    /// </summary>
    [HttpGet]
    [ProducesResponseType(typeof(IReadOnlyList<OrderDto>), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
    public async Task<IActionResult> GetAll(CancellationToken ct)
    {
        var result = await _mediator.Send(new GetOrdersQuery(), ct);
        return Ok(result);
    }
}
```

### ProducesResponseType Cheat Sheet

| Scenario | Attributes |
|----------|------------|
| **GET (list)** | `[ProducesResponseType(typeof(List<Dto>), 200)]` |
| **GET (single)** | `[ProducesResponseType(typeof(Dto), 200)]`<br>`[ProducesResponseType(typeof(ProblemDetails), 404)]` |
| **POST (create)** | `[ProducesResponseType(typeof(Guid), 201)]`<br>`[ProducesResponseType(typeof(ValidationProblemDetails), 400)]` |
| **PUT (update)** | `[ProducesResponseType(204)]`<br>`[ProducesResponseType(typeof(ProblemDetails), 404)]` |
| **DELETE** | `[ProducesResponseType(204)]`<br>`[ProducesResponseType(typeof(ProblemDetails), 404)]` |
| **Auth required** | Add `[ProducesResponseType(typeof(ProblemDetails), 401)]` |
| **Permission required** | Add `[ProducesResponseType(typeof(ProblemDetails), 403)]` |
| **Conflict possible** | Add `[ProducesResponseType(typeof(ProblemDetails), 409)]` |

### OpenAPI URLs

| Environment | URL | Purpose |
|-------------|-----|---------|
| Development | `/openapi/v1.json` | Raw OpenAPI spec |
| Development | `/scalar/v1` | Interactive UI (Scalar) |
| Production | Disabled | Security best practice |

---

## Structure

```
{{ProjectName}}.Api/
├── Controllers/                → Organized by Application/Module
├── Authorization/              → RequirePermissionAttribute
├── Middleware/
│   ├── GlobalExceptionHandlerMiddleware.cs
│   └── SessionValidationMiddleware.cs
├── Extensions/
├── RateLimiting/
├── Program.cs
├── appsettings.json
└── appsettings.Development.json
```

## Patterns

### Controller Template

```csharp
namespace {{ProjectName}}.Api.Controllers;

using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using {{ProjectName}}.Api.Authorization;

[ApiController]
[ApiExplorerSettings(GroupName = "integration")]
[Route("api/v1/integration/[controller]")]
[Produces("application/json")]
[Tags("Orders")]
public class OrdersController : ControllerBase
{
    private readonly ISender _mediator;

    public OrdersController(ISender mediator)
    {
        _mediator = mediator;
    }

    [HttpGet]
    [ProducesResponseType(typeof(IReadOnlyList<OrderDto>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAll(CancellationToken ct)
    {
        var result = await _mediator.Send(new GetOrdersQuery(), ct);
        return Ok(result);
    }

    [HttpGet("{id:guid}")]
    [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
    {
        var result = await _mediator.Send(new GetOrderByIdQuery(id), ct);
        return result is null ? NotFound() : Ok(result);
    }

    [HttpPost]
    [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Create([FromBody] CreateOrderCommand command, CancellationToken ct)
    {
        var id = await _mediator.Send(command, ct);
        return CreatedAtAction(nameof(GetById), new { id }, id);
    }

    [HttpPut("{id:guid}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Update(Guid id, [FromBody] UpdateOrderCommand command, CancellationToken ct)
    {
        if (id != command.Id) return BadRequest();
        await _mediator.Send(command, ct);
        return NoContent();
    }

    [HttpDelete("{id:guid}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
    {
        await _mediator.Send(new DeleteOrderCommand(id), ct);
        return NoContent();
    }
}
```

### Exception Middleware

```csharp
namespace {{ProjectName}}.Api.Middleware;

using System.Net;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using {{ProjectName}}.Application.Common.Exceptions;

public class GlobalExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;

    public GlobalExceptionHandlerMiddleware(RequestDelegate next, ILogger<GlobalExceptionHandlerMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (ValidationException ex)
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
            context.Response.ContentType = "application/json";
            var problemDetails = new ValidationProblemDetails(
                ex.Errors.GroupBy(e => e.PropertyName)
                    .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()))
            {
                Status = StatusCodes.Status400BadRequest,
                Title = "Validation failed"
            };
            await context.Response.WriteAsJsonAsync(problemDetails);
        }
        catch (NotFoundException ex)
        {
            context.Response.StatusCode = StatusCodes.Status404NotFound;
            context.Response.ContentType = "application/json";
            var problemDetails = new ProblemDetails
            {
                Title = "Not Found",
                Detail = ex.Message,
                Status = StatusCodes.Status404NotFound
            };
            await context.Response.WriteAsJsonAsync(problemDetails);
        }
        catch (DomainException ex)
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
            context.Response.ContentType = "application/json";
            var problemDetails = new ProblemDetails
            {
                Title = "Business rule violation",
                Detail = ex.Message,
                Status = StatusCodes.Status400BadRequest
            };
            await context.Response.WriteAsJsonAsync(problemDetails);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception: {Message}", ex.Message);
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = "application/json";
            var problemDetails = new ProblemDetails
            {
                Title = "Internal Server Error",
                Detail = "An unexpected error occurred",
                Status = StatusCodes.Status500InternalServerError
            };
            await context.Response.WriteAsJsonAsync(problemDetails);
        }
    }
}
```

## API Response Conventions

| Action | Status Code | Response |
|--------|-------------|----------|
| GET (list) | 200 | `List<Dto>` |
| GET (single) | 200 / 404 | `Dto` / ProblemDetails |
| POST | 201 | Id + Location header |
| PUT | 204 | Empty |
| DELETE | 204 | Empty |
| Validation error | 400 | ValidationProblemDetails |
| Server error | 500 | ProblemDetails |

## Rules

1. **Controllers are thin** - delegate to MediatR immediately
2. **NO business logic** in controllers
3. **Use `CancellationToken`** in all async methods
4. **Document with XML comments** for OpenAPI
5. **Return `IActionResult`** for flexibility
6. **ALWAYS add `[ProducesResponseType]`** for all possible responses
7. **ALWAYS add `[Tags]`** to group endpoints in OpenAPI

## When Adding New Endpoint

1. Create controller in `Controllers/` (or add to existing)
2. Inject `ISender` (MediatR)
3. Create action method with proper HTTP verb
4. Document with XML comments
5. Add `[ProducesResponseType]` attributes
6. Use command/query from Application layer
