Controller dan Routing

Routing atribut dan konvensi REST.

Daftar isi

Controller mengelompokkan endpoint yang berkaitan ke dalam satu kelas, sedangkan routing atribut menautkan setiap aksi ke pola rute yang dinyatakan di atasnya. Kombinasi keduanya merupakan bentuk klasik ASP.NET Core untuk membangun REST API, dengan TugasController yang menampung seluruh aksi daftar tugas pada satu tempat. Topik ini membahas struktur kelas controller, pendaftaran rute lewat atribut, dan konvensi REST yang mengatur metode HTTP beserta status responsnya, ditutup dengan demo controller yang dijalankan dan diuji dalam satu proses.

Controller dan aksi sebagai pengelompokan endpoint#

Controller adalah kelas turunan ControllerBase yang metode publiknya disebut aksi, yaitu endpoint yang dapat dipanggil klien. Atribut [Route] pada kelas menyatakan awalan rute untuk seluruh aksi di dalamnya, sedangkan atribut [HttpGet], [HttpPost], [HttpPut], dan [HttpDelete] pada aksi menyatakan metode HTTP yang ditangani beserta pola tambahannya. Cara ini disebut routing atribut (attribute routing) karena rute dinyatakan pada kode aksinya sendiri, bukan pada tabel terpisah yang harus dijaga tetap sinkron. Kelas TugasController dengan awalan api/tugas menjadi contoh pengelompokan tersebut untuk seluruh aksi daftar tugas.

Atribut [ApiController] pada kelas mengaktifkan perilaku otomatis yang dibutuhkan API, yaitu kewajiban setiap aksi memiliki rute atribut, penyimpulan sumber data setiap parameter, dan jawaban 400 otomatis untuk model yang tidak valid. Perilaku otomatis itu bekerja diam-diam pada demo, sehingga kode aksi hanya berisi logika rute dan respons. Layanan controller didaftarkan dengan builder.Services.AddControllers(), sedangkan app.MapControllers() membaca seluruh kelas controller saat aplikasi dimulai dan memasukkan rute atributnya ke tabel routing. Keduanya wajib ada karena tanpa MapControllers tidak ada endpoint controller yang terdaftar, sehingga semua request berujung 404 meskipun kelas controller sudah ditulis dengan benar.

Konvensi REST pada rute dan respons#

Konvensi REST mengatur hubungan antara metode HTTP, bentuk URL, dan status respons. URL menyatakan sumber daya (resource) dengan kata benda jamak seperti api/tugas, sedangkan metode HTTP menyatakan aksinya, yaitu GET untuk membaca, POST untuk membuat, PUT untuk memperbarui, dan DELETE untuk menghapus. Karena aksi sudah tercermin dari metode HTTP, URL tidak perlu memuat kata kerja seperti /ambil atau /tambahkan. Pola {id:int} pada aksi baca menyatakan parameter rute dengan batasan bilangan bulat, sehingga tiga aksi berikut cukup mengunggulkan tiga bentuk respons yang berbeda.

AksiAtribut dan ruteRespons yang pantas
Daftar semua tugas[HttpGet] pada api/tugas200 beserta JSON kumpulan tugas
Ambil satu tugas[HttpGet("{id:int}")]200 beserta JSON satu tugas, atau 404 bila tidak ada
Tambah tugas[HttpPost]201 beserta header Location menuju tugas baru

Status respons mengikuti hasil aksi, bukan selalu 200. Pembacaan yang berhasil membalas 200, pencarian yang gagal menemukan data membalas 404, dan pembuatan data baru membalas 201. Metode CreatedAtAction membangun respons 201 sekaligus menghitung URL data baru dari aksi AmbilSatu, sehingga header Location menunjuk alamat yang dapat langsung dipanggil kembali oleh klien. Respons 404 juga dapat diberi isi pesan lewat NotFound(new PesanError(...)), sehingga klien mengetahui alasan penolakannya.

Demo controller tugas dalam satu proses#

Program berikut menempatkan TugasController dalam berkas yang sama dengan pendaftaran aplikasi, sehingga demo tetap berupa satu berkas yang dapat dijalankan. Server diuji dengan lima request, yaitu tiga GET untuk membaca daftar lengkap, satu objek, dan data yang tidak ada, disusul satu POST untuk menambah tugas, dan satu GET penutup untuk membuktikan datanya bertambah.

csharp Program.cs
#:sdk Microsoft.NET.Sdk.Web
#:property MvcEnhancedModelMetadataSupport=true
#:property NoWarn=IL2026;IL3050

// Program: API dengan controller dan routing atribut yang diuji dalam satu proses
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// provider log dibuang agar keluaran demo tidak memuat baris log server
builder.Logging.ClearProviders();

// metadata JSON diarahkan ke konteks sumber hasil generasi di bagian bawah berkas
builder.Services.AddControllers();
builder.Services.Configure<JsonOptions>(o =>
    o.JsonSerializerOptions.TypeInfoResolver = KonteksJson.Default);

var app = builder.Build();

// MapControllers membaca kelas controller dan mendaftarkan seluruh rute atributnya
app.MapControllers();

app.Urls.Add("http://127.0.0.1:5152");
await app.StartAsync();

var klien = new HttpClient();
var alamat = "http://127.0.0.1:5152";

// helper: satu request mencetak baris status lalu isi responsnya
async Task CetakGet(string jalur)
{
    HttpResponseMessage res = await klien.GetAsync($"{alamat}{jalur}");
    Console.WriteLine($"GET {jalur} -> {(int)res.StatusCode}");
    string isi = await res.Content.ReadAsStringAsync();
    if (isi.Length > 0) Console.WriteLine(isi);
}

await CetakGet("/api/tugas");
await CetakGet("/api/tugas/2");
await CetakGet("/api/tugas/99");

// body JSON dikirim manual agar isi yang dikirim terlihat jelas
var isiBaru = new StringContent(
    """{"id":0,"judul":"Susun jadwal piket"}""",
    System.Text.Encoding.UTF8,
    "application/json");
HttpResponseMessage res = await klien.PostAsync($"{alamat}/api/tugas", isiBaru);
Console.WriteLine($"POST /api/tugas -> {(int)res.StatusCode}");
Console.WriteLine($"Location: {res.Headers.Location}");
Console.WriteLine(await res.Content.ReadAsStringAsync());

await CetakGet("/api/tugas");

await app.StopAsync();

[ApiController]
[Route("api/tugas")]
public class TugasController : ControllerBase
{
    // daftar statis berfungsi sebagai penyimpanan sederhana dalam memori
    private static readonly List<Tugas> daftar = new()
    {
        new Tugas(1, "Kirim laporan"),
        new Tugas(2, "Tinjau rancangan"),
    };

    [HttpGet]
    public IEnumerable<Tugas> DaftarSemua() => daftar;

    [HttpGet("{id:int}")]
    public ActionResult<Tugas> AmbilSatu(int id)
    {
        var tugas = daftar.FirstOrDefault(t => t.Id == id);
        return tugas is null ? NotFound(new PesanError("Tugas tidak ditemukan")) : tugas;
    }

    [HttpPost]
    public ActionResult<Tugas> Tambah(Tugas tugas)
    {
        var baru = tugas with { Id = daftar.Max(t => t.Id) + 1 };
        daftar.Add(baru);
        return CreatedAtAction(nameof(AmbilSatu), new { id = baru.Id }, baru);
    }
}

[JsonSerializable(typeof(List<Tugas>))]
[JsonSerializable(typeof(IEnumerable<Tugas>))]
[JsonSerializable(typeof(PesanError))]
[JsonSerializable(typeof(Tugas))]
internal partial class KonteksJson : JsonSerializerContext { }

public record Tugas(int Id, string Judul);
public record PesanError(string Pesan);

Dua arahan #:property di bagian atas berkas menyetel perilaku aplikasi satu berkas yang memakai controller. Saklar MvcEnhancedModelMetadataSupport=true menyiapkan metadata model MVC sejak build agar pengikatan (binding) parameter aksi berjalan, sedangkan NoWarn=IL2026;IL3050 membisukan peringatan analisis trimming dan AOT yang tidak relevan bagi demo. Kelas KonteksJson menyediakan metadata JSON untuk setiap tipe yang dikirim maupun diterima, dan konfigurasi lewat Configure<JsonOptions> diperlukan karena serialisasi respons dikelola MVC.

Jalankan program dari folder topik dengan perintah berikut.

bash
# aktifkan .NET yang terpasang di sandbox
export DOTNET_ROOT=/home/kal/.dotnet PATH=/home/kal/.dotnet:$PATH DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
# jalankan program dari folder topik
dotnet run --file Program.cs
text Keluaran
GET /api/tugas -> 200
[{"id":1,"judul":"Kirim laporan"},{"id":2,"judul":"Tinjau rancangan"}]
GET /api/tugas/2 -> 200
{"id":2,"judul":"Tinjau rancangan"}
GET /api/tugas/99 -> 404
{"pesan":"Tugas tidak ditemukan"}
POST /api/tugas -> 201
Location: http://127.0.0.1:5152/api/tugas/3
{"id":3,"judul":"Susun jadwal piket"}
GET /api/tugas -> 200
[{"id":1,"judul":"Kirim laporan"},{"id":2,"judul":"Tinjau rancangan"},{"id":3,"judul":"Susun jadwal piket"}]

Tiga GET pertama memperlihatkan respons daftar lengkap, satu objek untuk id 2, dan 404 berisi pesan JSON untuk id 99 karena NotFound menerima objek PesanError. Respons POST bernilai 201, header Location menunjuk api/tugas/3, dan isi responsnya adalah tugas baru dengan id yang dihitung dari daftar. GET penutup memuat tiga tugas, membuktikan daftar statis dalam memori bertambah dan tetap hidup sepanjang proses berjalan. Isi body yang dikirim memakai nama id dan judul karena pengikatan JSON dari klien mengikuti aturan camelCase yang sama dengan penamaan responsnya.

Kesalahan umum#

  • Lupa memanggil app.MapControllers(). Kelas controller sudah ditulis tetapi tidak ada endpoint yang terdaftar, sehingga seluruh request ke api/tugas dijawab 404. Pastikan AddControllers dan MapControllers dipasang berpasangan.
  • Membuat dua aksi dengan pola rute yang sama persis. Tabel routing menemukan dua endpoint yang sama-sama cocok, sehingga request ke rute itu digagalkan dengan error ambigu. Pastikan setiap pola rute unik di dalam satu controller.
  • Membalas pembuatan data dengan 200. Klien tidak dapat membedakan pembuatan berhasil dari pembacaan biasa dan tidak menerima alamat data barunya. Gunakan CreatedAtAction atau Created sehingga statusnya 201 beserta header Location.
  • Menulis aksi tanpa atribut metode HTTP seperti [HttpGet]. Aksi semacam itu menerima semua metode HTTP, sehingga endpoint yang semestinya hanya dibaca ikut menerima POST dan DELETE. Tambahkan atribut metode HTTP pada setiap aksi agar pemetaannya eksplisit.

Ringkasan#

  • Controller mengelompokkan aksi pada satu kelas turunan ControllerBase, dengan [Route] sebagai awalan rute dan atribut metode HTTP sebagai penanda aksi.
  • Routing atribut menyatakan rute pada kode aksinya, sedangkan [ApiController] menambahkan perilaku otomatis berupa kewajiban rute, penyimpulan sumber parameter, dan jawaban 400 untuk model tidak valid.
  • Konvensi REST memakai kata benda jamak pada URL dan memetakan GET ke 200, POST ke 201 beserta header Location, serta data yang tidak ditemukan ke 404.
  • AddControllers mendaftarkan layanan, sedangkan MapControllers mendaftarkan rute, dan keduanya wajib ada agar endpoint controller aktif.

Referensi#