REST API — Generated Code¶
The WebEndpointGenerator (triggered by [RestApi]) produces three categories of code: endpoint mappings, bootstrap extensions, and auth endpoints (when [WebAuth] is present). It also produces TypeScript client types for full-stack type safety.
Endpoint Mappings¶
For each handler annotated with an HTTP method attribute, the generator produces a Minimal API mapping inside a Map{RuntimeName}() extension method.
Input¶
[Runtime]
public partial class CatalogRuntime;
[DataStore]
public static partial class CatalogStore;
[DispatchModule]
public static partial class CatalogDispatch;
[RestApi(RoutePrefix = "/api/catalog", Title = "eShop Catalog API")]
public partial class CatalogApp;
public sealed record GetCatalogItemById(CatalogItemId Id) : IQuery;
public static class GetCatalogItemByIdHandler
{
[QueryHandler, HttpGet("/items/{id}")]
public static Eff<CatalogRuntime, Option<CatalogItem>> Handle(GetCatalogItemById query) =>
CatalogStore.CatalogItems.GetById<CatalogRuntime>(query.Id);
}
public sealed record UpdateCatalogItemPrice(CatalogItemId ItemId, decimal NewPrice) : ICommand;
public static class UpdateCatalogItemPriceHandler
{
[CommandHandler, HttpPut("/items/{catalogItemId}/price")]
public static Eff<CatalogRuntime, ProductPriceChanged> Handle(UpdateCatalogItemPrice cmd) =>
from item in CatalogStore.CatalogItems.Require<CatalogRuntime>(cmd.ItemId)
let oldPrice = item.Price
from _ in Mutate<CatalogRuntime>(() => item.Price = cmd.NewPrice)
>> CatalogStore.CatalogItems.Save<CatalogRuntime>(item)
select new ProductPriceChanged(cmd.ItemId, oldPrice, cmd.NewPrice);
}
Output¶
public static class CatalogAppExtensions
{
public static WebApplication MapCatalogApp(this WebApplication app)
{
var api = app.MapGroup("/api/catalog");
api.MapGet("/items/{id}", async (
[AsParameters] GetCatalogItemById query,
CatalogRuntime rt) =>
(await CatalogDispatch.Dispatch(query).RunAsync(rt)).Match(
Succ: option => option.Match(
Some: value => Results.Ok(value),
None: () => Results.NotFound()),
Fail: error => WebError.ToResult(error)));
api.MapPut("/items/{catalogItemId}/price", async (
UpdateCatalogItemPrice cmd,
CatalogRuntime rt) =>
(await CatalogDispatch.Dispatch(cmd).RunAsync(rt)).Match(
Succ: _ => Results.NoContent(),
Fail: error => WebError.ToResult(error)));
return app;
}
}
Key behaviors:
- GET queries bind via
[AsParameters]— record properties become route/query string params - POST/PUT/PATCH commands deserialize the request body
Option<T>return types automatically produce 404 onNone[Public]handlers skip the authorization requirement- Errors return appropriate HTTP status codes via
WebError.ToResult()
Bootstrap Extension¶
The generator produces an Add{RuntimeName}() extension method for DI registration with a typed options class.
public static class CatalogAppExtensions
{
public static IServiceCollection AddCatalogApp(
this IServiceCollection services,
Action<CatalogAppRestApiOptions>? configure = null)
{
var options = new CatalogAppRestApiOptions();
configure?.Invoke(options);
services.AddCors(cors =>
cors.AddDefaultPolicy(policy =>
policy.WithOrigins(options.CorsOrigins)
.AllowAnyMethod()
.AllowAnyHeader()));
if (options.OpenApi)
services.AddOpenApi();
services.ConfigureHttpJsonOptions(json =>
json.SerializerOptions.PropertyNamingPolicy = options.NamingPolicy);
return services;
}
}
RestApiOptions class¶
public sealed class CatalogAppRestApiOptions
{
public string[] CorsOrigins { get; set; } = ["*"];
public bool OpenApi { get; set; } = true;
public JsonNamingPolicy NamingPolicy { get; set; } = JsonNamingPolicy.CamelCase;
public CatalogAppRestApiOptions WithCorsOrigins(params string[] origins) { ... }
public CatalogAppRestApiOptions EnableOpenApi() { ... }
public CatalogAppRestApiOptions DisableOpenApi() { ... }
public CatalogAppRestApiOptions UseCamelCase() { ... }
public CatalogAppRestApiOptions UseSnakeCase() { ... }
}
Auth Endpoints¶
When [WebAuth("google")] is present on the [RestApi] class, the generator adds auth endpoint mappings:
// Inside MapCatalogApp():
api.MapPost("/auth/google/login", AuthEndpoints.HandleGoogleLogin);
api.MapPost("/auth/refresh", AuthEndpoints.HandleRefreshToken);
api.MapPost("/auth/logout", AuthEndpoints.HandleLogout);
The AuthEndpoints static class delegates to IJwtService, IGoogleTokenVerifier, and IRefreshTokenStore from DI.
TypeScript Output¶
The generator also produces TypeScript files for full-stack type safety:
Types¶
export interface GetCatalogItemByIdQuery {
id: string;
}
export interface GetCatalogItemByIdResult {
item: CatalogItem;
}
export interface UpdateCatalogItemPriceCommand {
id: string;
newPrice: number;
}
export interface UpdateCatalogItemPriceResult {
oldPrice: number;
newPrice: number;
}
Client¶
export class CatalogClient {
constructor(private baseUrl: string, private getToken: () => string | null) {}
async getCatalogItemById(query: GetCatalogItemByIdQuery): Promise<GetCatalogItemByIdResult> {
return this.get(`/api/catalog/items/${query.id}`);
}
async updateCatalogItemPrice(command: UpdateCatalogItemPriceCommand): Promise<UpdateCatalogItemPriceResult> {
return this.post(`/api/catalog/items/${command.id}/price`, command);
}
}
C# records map to TypeScript interfaces. TypedId properties become string. decimal becomes number. Collections become arrays.