Routing adalah mekanisme yang menentukan controller dan action method mana yang akan menangani sebuah request berdasarkan URL dan HTTP verb yang dikirim client. ASP.NET Core menyediakan attribute routing yang memungkinkan pola URL didefinisikan langsung di atas controller dan method menggunakan atribut.

Selain menentukan tujuan request, routing juga menangani ekstraksi parameter dari URL, baik dari segment path, query string, maupun body request, dan secara otomatis mengonversinya ke tipe data yang sesuai dengan parameter method melalui mekanisme model binding.

Artikel ini membahas cara mendefinisikan route dengan attribute routing, berbagai cara menerima parameter, serta konvensi penamaan route yang membuat API lebih mudah dipahami dan konsisten.

Attribute Routing Dasar

Route template menentukan pola URL yang dilayani setiap action

[ApiController] [Route("api/[controller]")] public class ProdukController : ControllerBase { [HttpGet] public IActionResult GetSemua() => Ok(GetDaftarProduk()); [HttpGet("{id}")] public IActionResult GetById(int id) => Ok(CariProduk(id)); } // GET /api/Produk -> GetSemua() // GET /api/Produk/5 -> GetById(5)
Token [controller] pada [Route("api/[controller]")] secara otomatis digantikan nama controller tanpa akhiran 'Controller', sehingga ProdukController akan melayani route /api/Produk. Parameter {id} pada route action menangkap bagian URL tersebut dan secara otomatis di-bind ke parameter method yang namanya cocok.

Mengambil Parameter dari Query String

FromQuery mengambil parameter dari query string URL

[HttpGet("cari")] public IActionResult Cari([FromQuery] string kataKunci, [FromQuery] int halaman = 1) { return Ok($"Mencari '{kataKunci}' halaman {halaman}"); } // GET /api/Produk/cari?kataKunci=laptop&halaman=2
Atribut [FromQuery] secara eksplisit menandakan parameter tersebut diambil dari query string URL, meski ASP.NET Core sebenarnya bisa menyimpulkan ini secara otomatis untuk tipe data sederhana seperti string dan int. Menuliskannya secara eksplisit tetap disarankan untuk kejelasan kode bagi developer lain yang membacanya.

Mengambil Data dari Body Request

FromBody mengambil dan mendeserialisasi JSON dari body request

[HttpPost] public IActionResult Tambah([FromBody] ProdukDto produk) { // produk otomatis di-deserialize dari JSON body request return CreatedAtAction(nameof(GetById), new { id = 1 }, produk); }
Atribut [FromBody] menandakan parameter tersebut diambil dari body request dan secara otomatis dideserialisasi dari JSON menjadi object C# sesuai tipe yang didefinisikan. CreatedAtAction mengembalikan response 201 Created beserta header Location yang menunjuk ke endpoint untuk mengambil resource yang baru dibuat tersebut.

Route Constraint untuk Validasi Tipe Parameter

Route constraint membedakan route berdasarkan tipe parameter

[HttpGet("{id:int}")] public IActionResult GetById(int id) => Ok(CariProduk(id)); [HttpGet("{kode:alpha}")] public IActionResult GetByKode(string kode) => Ok(CariProdukByKode(kode)); // /api/Produk/5 -> cocok dengan {id:int} // /api/Produk/ABC -> cocok dengan {kode:alpha}
Route constraint seperti :int atau :alpha memungkinkan dua route dengan pola serupa namun tipe parameter berbeda untuk didefinisikan tanpa ambiguitas, memastikan request diarahkan ke action method yang tepat berdasarkan format nilai parameter yang dikirim, bukan hanya berdasarkan posisinya dalam URL.

Selain parameter tunggal, model binding juga mendukung binding otomatis ke object kompleks dari kombinasi query string, misalnya sebuah class FilterProduk dengan beberapa property akan otomatis terisi dari beberapa parameter query string sekaligus tanpa perlu menuliskan setiap parameter satu per satu di signature method, selama nama property pada class cocok dengan nama parameter pada query string tersebut.

Attribute Routing Lanjutan: Route Constraint dan Konflik Rute

Route constraint memisahkan dua endpoint dengan pola segmen yang mirip

[ApiController] [Route("api/products")] public class ProductsController : ControllerBase { [HttpGet("{id:int:min(1)}")] public IActionResult GetById(int id) => Ok(new { id, name = "Produk A" }); [HttpGet("{code:alpha:length(3,10)}")] public IActionResult GetByCode(string code) => Ok(new { code }); } // GET /api/products/5 -> masuk ke GetById // GET /api/products/ABC123 -> masuk ke GetByCode // GET /api/products/abc! -> 404, tidak cocok constraint manapun

Route constraint seperti {id:int} atau {code:alpha} berfungsi sebagai filter tambahan pada proses pencocokan URL, bukan sekadar dokumentasi tipe data. Tanpa constraint, dua endpoint dengan template mirip seperti {id} dan {code} akan saling bentrok karena router tidak tahu endpoint mana yang dituju, dan ASP.NET Core akan melempar AmbiguousMatchException saat kedua rute sama-sama cocok dengan segmen yang diterima.

Constraint bawaan yang tersedia mencakup int, bool, datetime, decimal, guid, length(min,max), min(value), max(value), dan alpha, dan constraint ini bisa digabung dengan tanda titik dua berurutan seperti {id:int:min(1)} yang berarti segmen tersebut harus berupa integer sekaligus bernilai minimal satu. Kombinasi ini membuat validasi format URL terjadi di level routing, sebelum action method sempat dieksekusi sama sekali.

Penting dicatat bahwa route constraint bukan pengganti validasi input yang sesungguhnya, karena perannya hanya untuk membantu router memilih action yang tepat ketika ada beberapa kandidat rute yang mirip. Validasi bisnis seperti memastikan id tersebut benar-benar ada di database tetap harus dilakukan secara eksplisit di dalam action method atau lewat lapisan service terpisah.

Route Group dan Endpoint Metadata pada Minimal API

MapGroup menggabungkan path prefix, autentikasi, dan metadata OpenAPI sekaligus

var app = builder.Build(); // Route group dengan prefix dan otorisasi bersama var admin = app.MapGroup("/api/admin") .RequireAuthorization("AdminOnly") .WithTags("Admin"); admin.MapDelete("/users/{id:int}", async (int id, IUserService svc) => { await svc.DeleteAsync(id); return Results.NoContent(); }) .WithName("DeleteUser") .WithOpenApi(op => { op.Summary = "Menghapus user berdasarkan id"; return op; }); // Semua endpoint di dalam grup /api/admin otomatis butuh policy AdminOnly, // tanpa perlu menulis [Authorize] berulang di setiap endpoint

Pada pendekatan berbasis controller, pengelompokan rute biasanya dilakukan lewat [Route("api/[controller]")] di level class, sedangkan pada Minimal API peran serupa diambil alih oleh MapGroup yang memungkinkan sekumpulan endpoint berbagi prefix path, kebijakan otorisasi, dan metadata OpenAPI tanpa duplikasi konfigurasi di setiap endpoint individual. Fitur ini membuat Minimal API tetap terorganisir walau tidak memiliki struktur class formal seperti controller. MapGroup juga bisa disusun bertingkat, misalnya grup /api/admin dipecah lagi menjadi sub grup /api/admin/users dan /api/admin/reports yang masing-masing mewarisi pengaturan dari grup induknya.

Method RequireAuthorization() yang dipanggil pada level grup berlaku untuk seluruh endpoint di dalamnya, sehingga menambah endpoint admin baru ke dalam grup tersebut otomatis mewarisi aturan otorisasi yang sama tanpa risiko developer lupa menambahkan atribut [Authorize] pada endpoint baru tersebut. Pola ini mengurangi celah keamanan yang sering terjadi akibat kelalaian manusia saat menambah endpoint satu per satu. Filter tambahan seperti validasi header kustom pun bisa dipasang di level grup lewat AddEndpointFilter, berlaku konsisten untuk semua endpoint anggotanya.

WithName dan WithOpenApi memberi metadata tambahan yang dipakai oleh dokumentasi Swagger serta mekanisme pembuatan link seperti Results.CreatedAtRoute, mirip dengan nama route pada routing berbasis controller. Kombinasi metadata ini penting agar Minimal API yang terlihat sederhana secara kode tetap menghasilkan dokumentasi API yang informatif dan tidak kalah lengkap dibanding dokumentasi yang dihasilkan dari atribut controller tradisional. Tanpa WithOpenApi, Swagger tetap bisa membaca endpoint secara otomatis, tetapi deskripsi dan ringkasannya akan kosong atau hanya berupa nama method generik.

Content Negotiation: Mendukung Response JSON dan XML Sekaligus

builder.Services.AddControllers() .AddXmlSerializerFormatters(); [HttpGet("{id}")] public IActionResult GetProduk(int id) { var produk = _repo.GetById(id); return Ok(produk); } // GET /api/produk/1 dengan header Accept: application/xml // akan menghasilkan body XML, bukan JSON, tanpa mengubah kode action

Secara default ASP.NET Core hanya mengembalikan response dalam format JSON karena output formatter untuk XML tidak didaftarkan pada konfigurasi awal controller. Dengan menambahkan AddXmlSerializerFormatters, framework akan memilih formatter yang sesuai berdasarkan header Accept yang dikirim klien, sehingga action yang sama dapat melayani konsumen lama yang masih mengharapkan format XML tanpa perlu menulis controller atau endpoint terpisah khusus untuk kebutuhan tersebut.

Content negotiation jarang dibutuhkan jika seluruh konsumen API sudah sepakat memakai JSON sebagai format tunggal, sehingga menambahkan formatter XML tanpa kebutuhan nyata hanya akan menambah kompleksitas dan beban testing. Fitur ini lebih relevan ketika API harus menjembatani sistem lama yang hanya bisa memproses XML, misalnya integrasi dengan layanan pihak ketiga atau sistem legacy yang sudah berjalan sejak lama dan sulit diubah formatnya.

Kesalahan Umum yang Sering Terjadi

Membuat Route yang Ambigu Antar Action Method

Dua action method dengan route template yang bisa cocok dengan URL yang sama tanpa constraint pembeda akan menyebabkan AmbiguousMatchException saat aplikasi dijalankan dan route tersebut diakses.

Lupa Menandai Parameter Body dengan FromBody

Tanpa FromBody, ASP.NET Core akan mencoba mengambil parameter object kompleks dari query string atau form, bukan dari JSON body, menyebabkan parameter tersebut selalu null meski client sudah mengirim data dengan benar.

Tidak Konsisten dalam Penamaan Route Antar Endpoint

Campuran gaya penamaan route seperti /api/produk-list dan /api/DaftarProduk dalam satu aplikasi yang sama membuat API terasa tidak konsisten dan lebih sulit diingat oleh konsumen API.

Kapan Pendekatan Ini Tidak Cocok Dipakai

Attribute routing memberikan kejelasan karena definisi route berada tepat di atas action method yang bersangkutan, memudahkan developer melihat langsung URL apa yang akan menjalankan method tertentu tanpa perlu mencari konfigurasi terpisah.

Convention-based routing (routing berdasarkan konvensi tanpa atribut eksplisit) bisa mengurangi duplikasi penulisan route pada aplikasi dengan pola URL yang sangat konsisten, namun kurang eksplisit dan lebih sulit dilacak dibanding attribute routing untuk API dengan struktur URL yang bervariasi.

Ringkasan

Routing dan controller bekerja sama untuk mengarahkan request ke action method yang tepat, dengan attribute routing memberikan kejelasan definisi URL langsung di kode. Pahami berbagai sumber parameter seperti FromQuery dan FromBody, serta manfaatkan route constraint untuk menghindari ambiguitas antar endpoint yang serupa.

Seri Belajar .NET Core:

← Sebelumnya: Memahami Middleware pada ASP.NET Core

→ Selanjutnya: Model Binding dan Validation di ASP.NET Core