🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Pierwsze REST API w ASP.NET Core — od zera do CRUD

Pierwsze REST API w ASP.NET Core — endpointy CRUD GET/POST/PUT/DELETE z kodami statusu

Rozumiesz już protokół HTTP — metody, kody statusu, nagłówki. Czas postawić serwer, który tym mówi. W tym poście od pustego katalogu dojdziesz do działającego REST API z pełnym CRUD-em: listowanie, pobieranie po ID, tworzenie, aktualizacja i usuwanie zasobów — z poprawnymi kodami odpowiedzi, nie „wszystko zwraca 200″.

Projekt w 30 sekund

dotnet new web -o SklepApi
cd SklepApi
dotnet run

Szablon web daje najcieńszy możliwy start — jeden plik Program.cs, zero kontrolerów, zero ceremonii:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "API działa");

app.Run();

WebApplication.CreateBuilder konfiguruje serwer (Kestrel), wstrzykiwanie zależności, logowanie i konfigurację. app.MapGet rejestruje endpoint. app.Run() startuje nasłuch. To wszystko — masz działający serwer HTTP.

Model i magazyn danych

Zasób API to zwykła klasa C#. Zacznijmy od produktu i prostego magazynu w pamięci (bazę podłączymy później przez Entity Framework Core):

public record Produkt(int Id, string Nazwa, decimal Cena);

public class MagazynProduktow
{
    private readonly List<Produkt> _produkty = new()
    {
        new(1, "Klawiatura", 149.99m),
        new(2, "Mysz", 79.00m)
    };
    private int _nastepneId = 3;

    public List<Produkt> Wszystkie() => _produkty;
    public Produkt? Znajdz(int id) => _produkty.FirstOrDefault(p => p.Id == id);

    public Produkt Dodaj(string nazwa, decimal cena)
    {
        var produkt = new Produkt(_nastepneId++, nazwa, cena);
        _produkty.Add(produkt);
        return produkt;
    }

    public bool Usun(int id) => _produkty.RemoveAll(p => p.Id == id) > 0;
}

Rejestrujesz magazyn jako singleton, żeby przetrwał między żądaniami:

builder.Services.AddSingleton<MagazynProduktow>();

CRUD — pięć endpointów

Teraz mięso. Każdy endpoint mapuje metodę HTTP na operację i zwraca właściwy kod statusu:

// GET wszystkie — 200 z listą
app.MapGet("/api/produkty", (MagazynProduktow magazyn) =>
    Results.Ok(magazyn.Wszystkie()));

// GET po ID — 200 albo 404
app.MapGet("/api/produkty/{id:int}", (int id, MagazynProduktow magazyn) =>
{
    var produkt = magazyn.Znajdz(id);
    return produkt is null ? Results.NotFound() : Results.Ok(produkt);
});

// POST — 201 Created + nagłówek Location
app.MapPost("/api/produkty", (NowyProdukt dane, MagazynProduktow magazyn) =>
{
    var produkt = magazyn.Dodaj(dane.Nazwa, dane.Cena);
    return Results.Created($"/api/produkty/{produkt.Id}", produkt);
});

// DELETE — 204 albo 404
app.MapDelete("/api/produkty/{id:int}", (int id, MagazynProduktow magazyn) =>
    magazyn.Usun(id) ? Results.NoContent() : Results.NotFound());

Zwróć uwagę na dwie rzeczy. Po pierwsze — MagazynProduktow pojawia się jako parametr, a ASP.NET Core sam go wstrzykuje z kontenera DI; nie tworzysz go ręcznie. Po drugie — Results.Created, Results.NoContent, Results.NotFound to dokładnie te kody, które omawialiśmy przy statusach HTTP. API mówi kodem, nie polem error w treści.

DTO — nie przyjmuj encji z zewnątrz

Endpoint POST przyjmuje NowyProdukt, a nie Produkt. To celowe:

public record NowyProdukt(string Nazwa, decimal Cena);

Gdybyś przyjął Produkt, klient mógłby przesłać własne Id i nadpisać logikę serwera — to podatność zwana *over-posting* (mass assignment). DTO (Data Transfer Object) to osobny model wejścia, który zawiera wyłącznie pola, jakie klient ma prawo ustawić. Serwer sam decyduje o Id.

Nigdy nie binduj żądania wprost do encji bazodanowej. DTO na wejściu to nie ceremonia — to granica, na której odsiewasz pola, których klient nie powinien dotykać.

Model binding — skąd ASP.NET bierze dane

Framework sam wypełnia parametry endpointu z odpowiednich źródeł żądania:

ParametrŹródłoPrzykład
{id:int} w ścieżceroute/api/produkty/5id = 5
Typ złożony (NowyProdukt)body (JSON){"nazwa":"...","cena":9.99}
Typ prosty niebędący w routequery string?limit=10int limit
Zarejestrowany serwisDIMagazynProduktow magazyn

Dzięki temu nie parsujesz JSON-a ani nie czytasz Request.Query ręcznie — deklarujesz, czego potrzebujesz, a binding to dostarcza. Szczegóły dopasowania ścieżek i wymuszania źródła ([FromQuery], [FromHeader]) rozwija Minimal API i routing.

Walidacja — 400, zanim dane wejdą głębiej

Puste żądanie nie powinno tworzyć produktu „bez nazwy za 0 zł”. Najprostsza bramka to jawne sprawdzenie i 400 Bad Request:

app.MapPost("/api/produkty", (NowyProdukt dane, MagazynProduktow magazyn) =>
{
    if (string.IsNullOrWhiteSpace(dane.Nazwa) || dane.Cena <= 0)
        return Results.BadRequest("Nazwa jest wymagana, a cena musi być dodatnia.");

    var produkt = magazyn.Dodaj(dane.Nazwa, dane.Cena);
    return Results.Created($"/api/produkty/{produkt.Id}", produkt);
});

Testowanie API — OpenAPI

W .NET dodasz opis OpenAPI (dawniej Swagger) jednym pakietem, żeby klikalnie testować endpointy zamiast ręcznie sklejać żądania:

builder.Services.AddOpenApi();
// ...
app.MapOpenApi(); // dokument dostępny pod /openapi/v1.json

Pułapki pierwszego API

Pułapka #1 — synchroniczny dostęp do bazy. W tym poście magazyn jest w pamięci, więc metody są synchroniczne. Gdy podłączysz prawdziwą bazę, każda operacja I/O musi być async/await (await db.SaveChangesAsync()) — blokowanie wątku na I/O pod obciążeniem zabija przepustowość serwera.

Pułapka #2 — zwracanie encji zamiast DTO. Ten sam problem co na wejściu, tylko na wyjściu: zwracając encję bazodanową, wyciekasz pola wewnętrzne (hasła, flagi, klucze obce) i wiążesz kształt odpowiedzi ze schematem bazy. Mapuj encję na DTO odpowiedzi.

Pułapka #3 — POST bez Location. Results.Created wymaga URI utworzonego zasobu. Zwrócenie samego 201 bez nagłówka Location łamie kontrakt REST — klient nie wie, gdzie zasób teraz żyje.

Pułapka #4 — zły port / HTTPS w dev. Gdy „nie łączy się”, najczęściej patrzysz na zły port. Porty definiuje Properties/launchSettings.json; dotnet run wypisuje je w konsoli (Now listening on: https://localhost:7xxx).

Minimal API vs kontrolery

Powyższy kod to Minimal API. Alternatywą są kontrolery (klasy z atrybutami [ApiController], [HttpGet]). Który wybrać:

Minimal APIKontrolery
CeremoniaMinimalnaWięcej klas/atrybutów
Małe API, mikroserwis✅ idealneNadmiarowe
Duże API, wiele akcjiRozłazi się✅ czytelna struktura
Filtry, konwencjeOgraniczone✅ pełne wsparcie

Dla pierwszego API i mikroserwisów Minimal API wygrywa prostotą. Przy większym projekcie kontrolery porządkują kod — ale routing i tak działa na tych samych zasadach.

Podsumowanie

Działające REST API w ASP.NET Core to WebApplication, kilka Map* mapujących metody HTTP na operacje, magazyn wstrzykiwany przez DI i — co najważniejsze — poprawne kody statusu zwracane przez Results (200/201+Location/204/404/400). Dwie decyzje odróżniają zabawkę od API produkcyjnego: DTO na wejściu i wyjściu (zamiast gołych encji) oraz walidacja odsiewająca złe żądania na 400, zanim dotrą głębiej. Reszta — baza, routing, pipeline — dokłada się do tego szkieletu, nie zmienia go.

Co dalej

Endpointy już działają, ale skąd ASP.NET wie, że /api/produkty/5 to id = 5, a nie coś innego? Wyjaśnia to Minimal API i routing. Potem Middleware pokaże, co dzieje się z żądaniem, zanim trafi do Twojego endpointu, a Clean Architecture w .NET 10 — REST API — jak to wszystko poukładać w większym projekcie.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 Co dalej?

Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →