Minimal API로 시작하기
가장 적은 코드로 웹 API 를 띄웁니다.
주소 하나에 함수 하나
1.1에서 본 app.MapGet("/", () => "안녕하세요.") 가
Minimal API 입니다. 주소와 그 주소를 맡을 함수를 곧바로 잇습니다.
클래스를 만들지 않고, 상속받을 것도 없습니다.
이름이 Minimal 인 것은 기능이 적어서가 아니라 적어야 할 것이 적기 때문입니다. 모델 바인딩·JSON 변환·의존성 주입은 그대로 동작합니다. 2.2에서 볼 MVC 와 비교하면 나누어 두는 자리가 없다는 점이 다릅니다.
- 맞는 자리 — 화면 없이 데이터만 돌려주는 API, 주소가 수십 개를 넘지 않는 규모.
- 덜 맞는 자리 — 주소가 많아 파일을 나누어야 할 때, 화면(HTML)을 함께 그려야 할 때.
둘 중 하나만 선택해야 하는 것은 아닙니다. 한 프로젝트에 함께 둘 수 있습니다.
할일 API 하나
주소 다섯 개짜리 API 입니다. 데이터베이스 없이 목록 하나로만 다룹니다(3부에서 데이터베이스로 바꿉니다).
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); var todos = new List<Todo> { new(1, "우유 사기", false), new(2, "책 반납", true) }; app.MapGet("/todos", () => todos); app.MapGet("/todos/{id:int}", (int id) => todos.FirstOrDefault(t => t.Id == id) is { } found ? Results.Ok(found) : Results.NotFound()); app.MapPost("/todos", (Todo todo) => { todos.Add(todo); return Results.Created($"/todos/{todo.Id}", todo); }); app.MapDelete("/todos/{id:int}", (int id) => { var found = todos.FirstOrDefault(t => t.Id == id); if (found is null) return Results.NotFound(); todos.Remove(found); return Results.NoContent(); }); app.Run(); record Todo(int Id, string Title, bool Done);
이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다. 터미널에서
dotnet run 으로 띄운 뒤 아래처럼 불러 보십시오.
curl http://localhost:5227/todos curl -X POST http://localhost:5227/todos \ -H "Content-Type: application/json" \ -d '{"id":4,"title":"세차","done":false}'
/todos/abc 가 400이 아니라 404 인 것이
눈여겨볼 곳입니다. {id:int} 라고 적어 두면 숫자가 아닌
것은 이 주소에 맞지 않는 것으로 봅니다. 함수가 불리지도 않으므로
형식이 틀렸다는 안내를 낼 자리가 없습니다.
매개 변수는 어디서 오나
함수의 매개 변수를 보고 어디서 가져올지 알아서 정합니다. 따로 적지 않아도 되는 규칙이 있습니다.
| 매개 변수 | 가져오는 곳 |
|---|---|
| 이름이 주소의 자리표와 같음 | 주소 — "/todos/{id:int}" 의 int id |
| 그 밖의 단순 형식 | 질의 문자열 — ?q=우유&page=2 |
| 복합 형식(클래스·record) | 본문(JSON) |
| 등록해 둔 서비스 | 의존성 주입 |
기본값을 적어 두면 없을 때 그 값이 들어갑니다. 아래는 /search?q=우유 로 부른 것입니다.
app.MapGet("/search", (string? q, int page = 1) => new { q, page });
돌려준 값이 응답이 됩니다
함수가 돌려준 것을 보고 응답을 만듭니다. 그냥 값을 돌려주면 200과 JSON 이고,
상태 코드를 정하려면 Results 를 사용합니다.
| 돌려준 것 | 응답 |
|---|---|
| 값(객체·목록) | 200 · JSON |
| 문자열 | 200 · text/plain |
| Results.Ok(값) | 200 · JSON |
| Results.NotFound() | 404 · 본문 없음 |
| Results.Created(주소, 값) | 201 · Location 머리글 · JSON |
| Results.NoContent() | 204 · 본문 없음 |
Results 대신 TypedResults 를
사용하면 돌려주는 형식이 그대로 드러납니다. 테스트 코드에서 상태
코드를 형식으로 확인할 수 있고, 문서(OpenAPI)도 정확해집니다.
// 어느 쪽도 동작은 같습니다 Results.Ok(found) // 반환 형식은 IResult TypedResults.Ok(found) // 반환 형식은 Ok<Todo>
JSON 으로 바뀔 때의 규칙
C# 의 속성 이름은 Title 인데 응답에는 title 로 나갔습니다. ASP.NET Core 가 첫 글자를 소문자로 바꾸어 내보내기 때문입니다. 받을 때는 반대로 대소문자를 가리지 않습니다.
한글도 그대로 나갑니다. 그런데 아무 설정 없이 직렬화하면 그렇지 않습니다. 아래를 실행해 두 줄을 비교해보십시오.
using System.Text.Json; using System.Text.Encodings.Web; using System.Text.Unicode; var todo = new Todo(1, "우유 사기", false); // 아무것도 정하지 않았을 때 Console.WriteLine(JsonSerializer.Serialize(todo)); // ASP.NET Core 가 사용하는 것과 같게 맞추었을 때 var web = new JsonSerializerOptions(JsonSerializerDefaults.Web) { Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) }; Console.WriteLine(JsonSerializer.Serialize(todo, web)); record Todo(int Id, string Title, bool Done);
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
둘째 줄이 서버가 실제로 내보내는 것과 같습니다. 첫째 줄의 \uC6B0\uC720 도 잘못된 JSON 이 아닙니다. 읽는 쪽이 되돌려 읽으므로 담긴 값은 같습니다. 다만 사람이 눈으로 볼 때 읽히지 않아, ASP.NET Core 는 넓은 범위를 그대로 내보내도록 맞춰 두었습니다.
언제부터 있었나
최상위 문과 함께 들어왔습니다. 그 전에는 주소 하나를 잇는 데에도 컨트롤러 클래스가 있어야 했습니다.
이후로도 채워졌습니다. .NET 7에서 필터와 그룹이 들어와
공통 처리를 묶을 수 있게 되었고, .NET 8부터는
미리 컴파일(Native AOT)이 가능해 시작이 빨라졌습니다.
dotnet new webapiaot 가 그것입니다.
직접 해보기
MapPut 으로 할일 하나를 통째로 바꾸는 주소를
추가해보세요. 없는 번호면 404, 바꿨으면 204를 돌려줍니다.
/todos?done=true 처럼 불렀을 때 그 값만 돌려주고, 아무것도 붙이지 않으면 전부 돌려주도록 고쳐보세요.
bool? 로 받습니다./todos/99 와 /todos/abc 는 둘 다 404 입니다. 그런데 같은 404가 아닙니다. 무엇이 다른지, 그리고 부르는 쪽에서 어떻게 구분해 줄 수 있을지 적어보세요.
MapGet·MapPost로 주소와 함수를 곧바로 잇습니다. 클래스가 없습니다.- 매개 변수는 이름과 형식을 보고 주소·질의 문자열·본문·서비스 가운데서 옵니다.
- 값을 돌려주면 200과 JSON 이고, 상태 코드를 정하려면
Results·TypedResults를 사용합니다. - 속성 이름은 첫 글자가 소문자로 바뀌어 나가고, 받을 때는 대소문자를 가리지 않습니다.
- 주소 제약(
{id:int})에 맞지 않으면 함수가 불리지도 않고 404 입니다.