# HTTP APIs (`Rask.Api` / `Rask.Api.Client`)

Sometimes you need a real HTTP API — a mobile client, a partner integration, a webhook receiver, or
just `curl`. [CQRS remote dispatch](https://rask.sh/docs/guides/cqrs/) deliberately gives you none of that: its whole pitch is
that there is *no* `/api/*` endpoint to write, and its route is a framework-internal path carrying a
CLR type name. That is the right answer when the only caller is your own browser code, and the wrong
one when the caller is somebody else.

So write the endpoint the ordinary ASP.NET way. Rask does two things with it: hosts it properly, and
generates a typed client so *your* code never writes the URL twice.

- [Hosting](#hosting) — `AddRaskApi()` / `MapRaskApi()`, and the 404 that was missing
- [The typed client](#the-typed-client) — one client per controller, generated from the declaration
- [Minimal APIs](#minimal-apis) — the same, read from the `MapGet` calls themselves
- [Validation](#validation) — `[Required]` and your `AbstractValidator<T>`, async rules included
- [What gets a client method](#what-gets-a-client-method) — and what is reported instead

---

## Hosting

There is nothing to wire. Like every other battery, referencing the framework is what turns this on —
write a controller and it answers:

```csharp
var app = RaskApp.Create(args);
app.Run<App>();
```

Nothing about the endpoint itself is Rask-specific either:

```csharp
[ApiController]
[Route("api/posts")]
public sealed class PostsController(AppDb db) : ControllerBase
{
    [HttpGet("{id:int}")]
    public async Task<ActionResult<Post>> Get(int id) =>
        await db.Posts.Get(id) is { } post ? post : NotFound();
}
```

### Order is not what makes this work

It is worth being blunt about this, because Rask's own docs used to say the opposite. Endpoint routing
matches on **precedence**, never on registration order, and every route you write is more specific than
Rask's `/{**path}` catch-all. Your endpoints answer from either side of `MapRask`. `MapEndpoints` is a
readable place to put them, not a fix for a bug.

What order *cannot* fix — and what `MapRaskApi()` does — is a request under your API prefix that
matches **nothing**. Without a guard it reaches the catch-all and renders the app, so a mistyped or
deleted route answers `200` with a web page and the caller's JSON parse fails a long way from the
cause. `MapRaskApi()` answers `404` with an RFC 9457 problem document under `ApiOptions.Prefix`
(default `/api`) and leaves every other path alone.

That guard is an ordinary endpoint rather than a fallback, deliberately: a fallback sits at
`int.MaxValue` and would lose to the catch-all, while at the same order `/api/{**rest}` outranks
`/{**path}` — and loses in turn to every real route beneath it.

```csharp
app.Configure(c =>
{
    c.Api.Off();                       // no HTTP endpoints in this app at all
    c.Api.Configure(o =>
    {
        o.Prefix = "/services";        // where this app's endpoints live
        o.NotFound = false;            // answer for unmatched paths yourself
        o.Controllers = false;         // minimal APIs only; never discover a controller
    });
});
```

An app assembled by hand, without `RaskApp`, calls `AddRaskApi()` and `MapRaskApi()`
itself. `MapRaskApi()` returns the endpoint group, so rate limiting, CORS or output caching attach to
the whole API in one line: `app.MapEndpoints(e => e.MapRaskApi().RequireRateLimiting("api"));`

### What gets registered

`AddMvcCore().AddDataAnnotations()`, not `AddControllers()`. An API controller needs the core —
routing, model binding, the JSON formatters, the `[ApiController]` conventions — while `AddControllers`
layers on the API explorer, CORS services and formatter mappings: machinery for OpenAPI documents,
cross-origin policies and `.json`-style URL suffixes that an app gets whether it asks or not.

DataAnnotations is the one addition, because dropping it changes **behaviour** rather than weight: a
`[Required]` on a request body silently stops being enforced, and an endpoint that quietly accepts what
it used to reject is worse than a heavier registration. (Checked by
`A_required_member_is_still_enforced_by_the_lean_registration`, which goes red without it.) An app that
wants CORS or an OpenAPI document adds `AddCors()` or `AddApiExplorer()` — a line it would have written
anyway.

---

## The typed client

The endpoint you already wrote is the source of truth. `Rask.Api.Client` reads it and generates one
client per controller — `PostsController` becomes `PostsClient` — so a component asks for the data
rather than for a URL:

```csharp
host.Services.AddRaskApiClient();
```

```csharp
public sealed class PostDetail(PostsClient posts) : Component
{
    [Prop] public int Id { get; init; }

    private Post? _post;

    protected override async Task OnMount() => _post = await posts.Get(Id);

    public override Component Render() =>
        _post is null ? Div["Loading…"] : Article[H1[_post.Title]];
}
```

Compare that with what `docs/http-and-files.md` describes, which is still exactly right for calling
*somebody else's* API:

```csharp
await http.GetFromJsonAsync<Post>($"/api/posts/{id}");   // four things that can drift
```

The URL, the verb, the request shape and the response shape are all restated at the call site, and
nothing checks them against the server. Rename the route on the controller and this compiles fine and
404s at run time. With the generated client it does not compile — which is the whole point.

The codecs are the reflection-free ones the CQRS wire uses, so a shape means the same thing on either,
and the client publishes clean under the WASM/AOT trimmer.

### In a one-project `wasm-hosted` app

The client reaches the browser app in `Client/` on its own. That is worth spelling out, because the
controller it was generated from cannot: the browser app compiles only `Client/` and `Shared/`, so a
generator running there would see no controllers at all.

What crosses instead is the **file** the server's own generator wrote. `EmitCompilerGeneratedFiles`
puts it on disk, and the browser app's generated project compiles that exact file — so the two halves
cannot disagree about the client, because there is only one of it. The client runtime
(`Rask.Api.Client`) is added to the browser app automatically when that file exists; you do not declare
it twice.

Deliberately unlike [`Rask.Cqrs.Client`](https://rask.sh/docs/guides/cqrs/), which must be kept *out* of the server: that split
exists because `AddRaskCqrsClient()` rewrites a process-wide registry, so a server holding it would
bounce its own messages straight back out. A typed API client has no such property — it dials routes
that are public anyway.

> **The shapes that cross the wire live in `Shared/`.** A request or response type declared anywhere else
> on the server side is invisible to the browser app, so the generated client returns a type it cannot
> compile — and you meet it as `CS0246` inside generated code you never wrote. Same rule as a CQRS
> message record: the **handler** is server-only, the **shape** is shared. In practice: `Post`, `NewPost`
> and friends in `Shared/`, `PostsController` outside `Client/` and `Shared/`.

### Securing an endpoint

`[Authorize]` works the way it does in any ASP.NET app — on the controller, on the action, with a policy
or roles, and `[AllowAnonymous]` to open one route on an otherwise protected controller:

```csharp
[ApiController]
[Route("api/orders")]
[Authorize]
public sealed class OrdersController(AppDb db) : ControllerBase
{
    [HttpGet("{id:int}")]
    public Task<ActionResult<Order>> Get(int id) => ...;

    [HttpDelete("{id:int}")]
    [Authorize(Roles = "admin")]
    public Task Cancel(int id) => ...;
}
```

Nothing about it is Rask-specific, and the lean registration does not weaken it: enforcement is asserted
over real HTTP in `AuthorizeTests` — 401 anonymous, 403 for a role the caller lacks, 200 for one they
hold — because an `[Authorize]` that silently stopped being enforced would still *look* like protection
in the source.

Your app still calls `AddAuthentication`/`AddAuthorization` and `UseAuthentication`/`UseAuthorization`;
the [accounts battery](https://rask.sh/docs/guides/authentication/) registers them, so a scaffolded app already has them. Their
absence fails loudly at startup rather than quietly per request.

On the client side, attach the token with `ApiClientOptions.ConfigureRequest` — it receives the
request rather than the `HttpClient`, so a token is scoped to the call instead of becoming ambient state
shared by everything that resolves the same client.

Unlike CQRS remote dispatch, none of this is re-implemented: there, two shared endpoints cannot carry
per-message metadata, so `Rask.Cqrs.Server` reads `[Authorize]` off the handler at compile time and
enforces it imperatively. An API controller *is* its own endpoint, so ASP.NET's own authorization
applies and Rask stays out of the way.

**The caller is also ambient.** In a Rask app with its data layer on, every HTTP request — a controller, a
minimal API endpoint, a CQRS endpoint — runs with its signed-in user reachable from
[`Current`](https://rask.sh/docs/guides/data/#the-current-user--current): `Current.UserId` is the caller's id, and a
[tenant-scoped](https://rask.sh/docs/guides/multi-tenancy/) read filters to the caller's tenant with nothing passed to it. It is set after
authentication, from `HttpContext.User`, so an action and the static factory it calls agree on who is asking —
the factory has no `User` property to read. `ControllerBase.User` is still there, and says the same.

### Failures

A call that does not succeed throws `ApiException`. `StatusCode` is `null` when the request never
reached a server at all — DNS, TLS, a dropped connection, a timeout — with the cause in
`InnerException`. A status means the server answered and said no. Those need different handling, and an
exception that blurred them would make "is it down, or am I wrong?" unanswerable:

```csharp
catch (ApiException ex) when (ex.StatusCode is null) { /* offline: queue it */ }
catch (ApiException ex) when (ex.StatusCode == 404)  { /* gone: show empty  */ }
catch (ApiException ex) when (ex.Errors is { } fields) { /* rejected: show them */ }
```

`Errors` is populated only for a [validation rejection](#validation), keyed by field with the empty key
holding rules about the request as a whole — the same map `RemoteDispatchException.Errors` carries. It
is the one failure whose text is safe to show: a validation message was authored for whoever sent the
request, which is why it crosses the wire when handler exception text does not.

`ApiClientOptions.ConfigureRequest` is the hook for a bearer token or a tenant header. It receives
the *request* rather than the `HttpClient`, deliberately: a token on the client is ambient state shared
by everything that resolves it, while a token here is scoped to the call being made.

---

## Minimal APIs

Read from the `MapGet`/`MapPost`/… invocations themselves, with no attribute and no registration:

```csharp
app.MapEndpoints(e =>
{
    e.MapGet("/api/widgets/{id:int}", async (int id, AppDb db) =>
        await db.Widgets.Get(id) is { } w ? TypedResults.Ok(w) : Results.NotFound());

    e.MapPost("/api/widgets", (Widget body, AppDb db) => db.Add(body));
});
```

A minimal API has no controller to be named after, and most live in a `Program.cs` whose enclosing type
is `Program` — a name that would tell a reader nothing. So they group by **route**: everything under
`/api/widgets` lands on `WidgetsClient`. The method name comes from the verb and the route past that
grouping segment, so `GET /api/widgets/{id}` becomes `GetById`. Chain `.WithName("…")` to choose it
yourself — that is ASP.NET's own way of naming an endpoint, not a Rask invention:

```csharp
e.MapDelete("/api/widgets/{id:int}/tag", (int id) => TypedResults.NoContent())
    .WithName("Untag");     // widgets.Untag(3)
```

**`TypedResults` is read properly.** `Ok<T>`, `NoContent` and `Results<Ok<T>, NotFound>` all carry the
response type in the signature, and the alternative carrying a body supplies the client's return type.
`Results.Ok(x)` — the untyped `IResult` — does not, and reports [RASK070](https://rask.sh/docs/guides/diagnostics/#rask070).

**An endpoint that is not part of the app's API says so** with `.ExcludeFromDescription()` — the same call
that keeps it out of OpenAPI — and the client leaves it out: no method, and no RASK070 about one.

---

## Validation

An endpoint is validated like everything else in a Rask app, with nothing declared: the body's
DataAnnotations attributes, then the `AbstractValidator<T>` written for its type — including
`MustAsync` rules, which neither `ModelState` nor `Validator.TryValidateObject` can run.

```csharp
public sealed class NewOrder
{
    [Required] public string? Reference { get; set; }
    [Range(1, 100)] public int Quantity { get; set; }
}
```

A failure answers `400` `application/problem+json` with an `errors` object keyed by field, under the
stable `type` documented at [a rejected request](https://rask.sh/docs/guides/validation/#a-rejected-request) — the same document a
rejected CQRS dispatch sends, so a client handles either. The generated client surfaces it as
`ApiException.Errors`.

Controllers are covered wherever they live. A minimal API is covered when it is mapped through
`app.MapEndpoints(e => …)`; one mapped straight onto the built `WebApplication` asks for the convention
by name with `.RequireRaskValidation()`, because ASP.NET has no global endpoint filter.

Full detail — the off switch, what it leaves to ASP.NET, and the client side — is in
[validation.md](https://rask.sh/docs/guides/validation/#http-endpoints).

---

## What gets a client method

Everything whose shape can be written as one, and a diagnostic naming the reason for everything else.
A method silently missing from a client reads as a broken generator, so nothing is skipped in silence.

| Written on the server | In the client |
|---|---|
| `ActionResult<Post>`, `Task<Post>`, `Post` | `Task<Post>` |
| `Task`, `void`, `NoContent` | `Task` |
| `Ok<Post>`, `Results<Ok<Post>, NotFound>` | `Task<Post>` |
| `IActionResult` + `[ProducesResponseType(typeof(Post), 200)]` | `Task<Post>` |
| `IActionResult` alone | nothing — [RASK070](https://rask.sh/docs/guides/diagnostics/#rask070) |

Parameters follow ASP.NET's own `[ApiController]` binding rules: a name matching a route token binds
from the route, a simple type from the query, and the one complex type left is the body. `[FromRoute]`,
`[FromQuery]`, `[FromBody]` and `[FromHeader]` say so explicitly.

Anything the caller does not supply is **not** a client parameter — `[FromServices]`, `HttpContext`,
`CancellationToken`, `ClaimsPrincipal`, `IFormFile`. An injected service is recognised without
`[FromServices]` too: a parameter only reaches the client's signature if its type is one the wire can
carry, and an interface or abstract type is exactly the shape of something that comes from the
container.

Two things worth knowing because they are easy to get wrong by hand:

- **Optional parameters keep the server's default.** An `int page = 1` generates `page = 1`, not
  `page = default` — which would send `page=0` whenever a caller omitted it and quietly override the
  server's own default with a zero that type-checks everywhere.
- **The client asks for JSON.** An action returning `string` otherwise answers `text/plain`, because
  ASP.NET's `StringOutputFormatter` wins content negotiation, and `return "ok"` would arrive as `ok`
  rather than `"ok"`.

Diagnostics: [RASK067](https://rask.sh/docs/guides/diagnostics/#rask067) (no wire encoding), [RASK068](https://rask.sh/docs/guides/diagnostics/#rask068)
(endpoint skipped, with the reason), [RASK069](https://rask.sh/docs/guides/diagnostics/#rask069) (two endpoints claiming one
client method), [RASK070](https://rask.sh/docs/guides/diagnostics/#rask070) (response type not statically known).

---

See also: [CQRS](https://rask.sh/docs/guides/cqrs/) for when the only caller is your own browser code and no HTTP surface needs to
exist, [HTTP & files](https://rask.sh/docs/guides/http-and-files/) for calling APIs you did not write, and
[Authentication](https://rask.sh/docs/guides/authentication/) for securing endpoints with `[Authorize]`.
