ASP.NET Core Minimal API 패턴
Minimal API는 ASP.NET Core 6에서 도입된 경량 웹 API 구축 방식입니다. 컨트롤러 클래스와 어트리뷰트 기반 라우팅 없이 람다 표현식으로 HTTP 엔드포인트를 직접 정의합니다. 마이크로서비스, serverless 함수, 프로토타입에 특히 적합합니다.
1. 최소 구성
섹션 제목: “1. 최소 구성”var builder = WebApplication.CreateBuilder(args);var app = builder.Build();
app.MapGet("/", () => "Hello, World!");
app.Run();2. 기본 라우팅 패턴
섹션 제목: “2. 기본 라우팅 패턴”var builder = WebApplication.CreateBuilder(args);builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();app.UseSwaggerUI();
// GETapp.MapGet("/products", () => new[] { "laptop", "phone", "tablet" });
// GET with route parameterapp.MapGet("/products/{id:int}", (int id) => $"Product {id}");
// POST with bodyapp.MapPost("/products", (ProductRequest req) =>{ var product = new Product { Id = 1, Name = req.Name, Price = req.Price }; return Results.Created($"/products/{product.Id}", product);});
// PUTapp.MapPut("/products/{id:int}", (int id, ProductRequest req) =>{ // 업데이트 로직 return Results.NoContent();});
// DELETEapp.MapDelete("/products/{id:int}", (int id) => Results.NoContent());
app.Run();
record ProductRequest(string Name, decimal Price);record Product { public int Id { get; init; } public string Name { get; init; } = ""; public decimal Price { get; init; } }3. 의존성 주입
섹션 제목: “3. 의존성 주입”// 서비스 등록builder.Services.AddScoped<IProductRepository, ProductRepository>();builder.Services.AddScoped<IProductService, ProductService>();
// 엔드포인트에서 DI (매개변수로 자동 주입)app.MapGet("/products/{id}", async ( int id, IProductService service, CancellationToken ct) =>{ var product = await service.GetByIdAsync(id, ct); return product is null ? Results.NotFound() : Results.Ok(product);});
// HttpContext 직접 접근app.MapGet("/me", (HttpContext ctx) => Results.Ok(new { UserAgent = ctx.Request.Headers.UserAgent.ToString() }));4. Results 헬퍼
섹션 제목: “4. Results 헬퍼”// 다양한 HTTP 응답 반환app.MapPost("/login", async (LoginRequest req, IAuthService auth) =>{ if (string.IsNullOrWhiteSpace(req.Username)) return Results.BadRequest("Username is required");
var token = await auth.AuthenticateAsync(req.Username, req.Password); if (token is null) return Results.Unauthorized();
return Results.Ok(new { Token = token });});
// TypedResults (C# 타입 추론, OpenAPI 문서 자동 생성)app.MapGet("/users/{id}", async Task<Results<Ok<User>, NotFound>> (int id, IUserRepo repo) =>{ var user = await repo.FindAsync(id); return user is not null ? TypedResults.Ok(user) : TypedResults.NotFound();});5. 엔드포인트 그룹화 (RouteGroupBuilder)
섹션 제목: “5. 엔드포인트 그룹화 (RouteGroupBuilder)”// 공통 prefix와 미들웨어를 그룹으로 묶기var products = app.MapGroup("/api/products") .WithTags("Products") .RequireAuthorization();
products.MapGet("/", async (IProductService svc) => Results.Ok(await svc.GetAllAsync()));
products.MapGet("/{id}", async (int id, IProductService svc) =>{ var p = await svc.GetByIdAsync(id); return p is null ? Results.NotFound() : Results.Ok(p);});
products.MapPost("/", async (ProductRequest req, IProductService svc) =>{ var created = await svc.CreateAsync(req); return Results.Created($"/api/products/{created.Id}", created);});
// 중첩 그룹var admin = app.MapGroup("/api/admin") .RequireAuthorization("AdminPolicy");
var adminProducts = admin.MapGroup("/products");adminProducts.MapDelete("/{id}", async (int id, IProductService svc) =>{ await svc.DeleteAsync(id); return Results.NoContent();});6. 요청 검증
섹션 제목: “6. 요청 검증”using FluentValidation;
// 검증기 등록builder.Services.AddScoped<IValidator<ProductRequest>, ProductRequestValidator>();
public class ProductRequestValidator : AbstractValidator<ProductRequest>{ public ProductRequestValidator() { RuleFor(x => x.Name).NotEmpty().MaximumLength(100); RuleFor(x => x.Price).GreaterThan(0); }}
// 필터로 자동 검증app.MapPost("/products", async ( ProductRequest req, IValidator<ProductRequest> validator, IProductService svc) =>{ var validation = await validator.ValidateAsync(req); if (!validation.IsValid) return Results.ValidationProblem(validation.ToDictionary());
var created = await svc.CreateAsync(req); return Results.Created($"/products/{created.Id}", created);});7. 필터 (EndpointFilter)
섹션 제목: “7. 필터 (EndpointFilter)”// 로깅 필터public class LoggingFilter : IEndpointFilter{ private readonly ILogger<LoggingFilter> _logger; public LoggingFilter(ILogger<LoggingFilter> logger) => _logger = logger;
public async ValueTask<object?> InvokeAsync( EndpointFilterInvocationContext context, EndpointFilterDelegate next) { var path = context.HttpContext.Request.Path; _logger.LogInformation("Request: {Path}", path); var result = await next(context); _logger.LogInformation("Response: {Path}", path); return result; }}
// 적용app.MapGet("/products", async (IProductService svc) => Results.Ok(await svc.GetAllAsync())) .AddEndpointFilter<LoggingFilter>();8. OpenAPI / Swagger 문서화
섹션 제목: “8. OpenAPI / Swagger 문서화”app.MapGet("/products/{id}", async (int id, IProductService svc) =>{ var p = await svc.GetByIdAsync(id); return p is null ? Results.NotFound() : Results.Ok(p);}).WithName("GetProduct").WithSummary("Get a product by ID").WithDescription("Returns a single product matching the given ID").WithTags("Products").Produces<Product>(200).Produces(404).RequireAuthorization();9. 테스트
섹션 제목: “9. 테스트”using Microsoft.AspNetCore.Mvc.Testing;
public class ProductApiTests : IClassFixture<WebApplicationFactory<Program>>{ private readonly HttpClient _client;
public ProductApiTests(WebApplicationFactory<Program> factory) { _client = factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { // 테스트용 서비스 교체 services.AddScoped<IProductRepository, InMemoryProductRepository>(); }); }).CreateClient(); }
[Fact] public async Task GetProduct_Returns200_WhenExists() { var response = await _client.GetAsync("/products/1"); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var product = await response.Content.ReadFromJsonAsync<Product>(); Assert.NotNull(product); Assert.Equal(1, product.Id); }
[Fact] public async Task GetProduct_Returns404_WhenNotFound() { var response = await _client.GetAsync("/products/9999"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); }}Minimal API는 컨트롤러 기반 API보다 낮은 오버헤드와 빠른 시작 시간을 제공합니다. MapGroup으로 엔드포인트를 구조화하고, TypedResults로 OpenAPI 문서를 자동화하며, IEndpointFilter로 횡단 관심사를 처리하면 컨트롤러 API와 동등한 기능성을 유지하면서도 코드가 훨씬 간결해집니다.