Routing, di mana setiap request.
Method HTTP menjawab apa dari sebuah request, maksud Anda. Routing menjawab di mana: resource mana di server yang jadi sasaran maksud Anda. Manual ini membahas routing dari ujung ke ujung, route statis dan dinamis, parameter path dan query, nesting, versioning, dan catch-all, dengan router yang sudah jadi di Go dan Python.
Apa Itu Routing
Setiap request membawa dua hal sekaligus. Method HTTP menjawab "apa yang ingin kulakukan?", ambil, tambah, perbarui, atau hapus data. Itu maksud Anda, aksinya. Route menjawab "di mana yang ingin kulakukan?", resource mana, di alamat mana di server, tempat aksi itu mau dilakukan.
Anda harus memberi tahu server ke mana harus pergi. Kirim request GET (maksud: ambil) ke route /users (alamatnya), dan server membalas dengan larik users. Routing adalah mesin yang mengambil apa dan di mana, memetakannya ke handler tertentu, lalu membiarkan handler itu menjalankan logika bisnis dan mengembalikan data.
Routing adalah memetakan path URL (plus method HTTP) ke logika sisi server: sebuah handler yang menjalankan logika bisnis, operasi database, dan apa pun yang diperlukan, lalu mengembalikan respons. Itu saja.
Istilah yang dipakai di manual ini
- Maksud / aksi: apa yang ingin dilakukan request, diekspresikan oleh method HTTP (ambil, tambah, perbarui, hapus).
- Resource: "benda" yang dikenai aksi request, biasanya kata benda di path seperti books atau users. (Resource dipelajari sungguh-sungguh di REST API; di sini kita perlakukan sekadar sebagai sasaran route.)
- Route / route path / URL path: bagian alamat yang menyebut di mana di server Anda pergi, mis. /api/books.
- Handler: rangkaian instruksi yang dijalankan server saat request cocok dengan route; ia pemilik logika untuk endpoint itu.
Method + Path -> Handler
Ini jantung routing. Server mengambil method dan route, lalu memperlakukan keduanya sebagai kunci unik yang memetakan ke satu handler. Method diperiksa dulu, lalu route, keduanya digabung, dan terbentuklah satu jalur routing unik.
Ambil dua request ke alamat yang sama. GET /api/books mengambil buku; POST /api/books membuat satu. Route-nya identik, tapi method-nya berbeda, jadi kuncinya berbeda dan keduanya menuju handler yang berbeda. Method membedakan dua route yang sebenarnya identik, dan kunci-kuncinya tidak pernah bentrok.
Routing only needs to know the method as the other half of the key. The common verbs and their intent: GET fetch / POST add/create / PUT / PATCH update / DELETE remove. The deep study of methods belongs to the HTTP topic; here they simply pair with a route to form the key.
Route Statis
Route statis adalah path tetap tanpa parameter variabel di dalamnya. /api/books statis: string-nya konsisten, ia sebuah konstanta. Anda selalu memakai string yang sama saat membuat request, tidak ada yang berubah di dalamnya, dan ia selalu mengembalikan jenis respons yang sama. Itulah persis kenapa disebut "statis".
String path konstan tanpa segmen dinamis, mis. /api/books. Dua contoh pertama dari sumber, GET dan POST di /api/books, keduanya route statis.
Mencocokkan route statis adalah perbandingan string literal. Kebanyakan API mulai di sini: satu endpoint yang mendaftar koleksi dan satu yang membuat item, keduanya di path konstan yang sama.
Route Dinamis & Parameter Path
Route dinamis punya parameter variabel di dalam path, jadi satu pola bisa melayani banyak resource spesifik. GET /api/users/123 mengambil detail satu pengguna, angka 123 adalah ID pengguna, yang diekstrak server dari route untuk mengerjakan tugasnya (ambil pengguna itu dari database lalu kembalikan).
Di sisi server, definisi route untuk ini tampak seperti method plus pola. Jika ditulis, bagian pencocokannya kira-kira seperti:
r.get("/api/users/:id", handler) // +--- ":id" = a dynamic parameter slot
Bagian method mencocokkan GET; bagian path mencocokkan /api/users/ diikuti string apa pun. Konvensi titik dua, :id, berbunyi "segmen ini dinamis; klien memasok nilai di sini". Konvensi :name ini Anda temukan di Node, Python, Go, Rust, dan Java; ini praktik seluruh industri.
Nilai dinamis yang hidup di dalam path, tepat setelah garis miring. Karena ia bagian dari route, ia disebut path parameter atau route parameter, dan ia membawa makna semantik: ia mengidentifikasi resource mana yang dimaksud request.
- Semua yang ada di path tiba sebagai string. 123 tampak seperti angka, tapi di route path angka dan karakter khusus semuanya diperlakukan sebagai string; handler meng-cast-nya kalau perlu.
- Dibaca lantang, GET /api/users/123 berbunyi "ambil data pengguna yang ID-nya 123". Konstruk yang bisa dibaca manusia ini adalah seluruh gagasan di balik routing gaya REST, memberi route makna semantik yang jelas.
Parameter Query
Parameter query adalah pasangan key/value yang menempel di ujung route setelah tanda tanya. Beberapa pasangan disambung dengan ampersand. Contohnya, /api/search?query=some+value, route-nya /api/search, dan setelah ? datang key query dengan nilai some+value.
Kenapa parameter query ada
Request POST dan PUT punya body: wadah untuk mengirim data yang ditentukan pengguna ke server. Tapi request GET, menurut konvensi REST, tidak punya body. Lalu bagaimana GET mengirim nilai tambahan? Parameter query jawabannya: mereka membuat GET mengangkut seperangkat pasangan key/value yang membawa metadata tentang request: bukan identitas resource, melainkan instruksi untuk membentuk respons.
Pasangan key=value opsional di query string, dipakai untuk filter, sort, paginate, atau cari. Biasanya dipakai dengan request GET karena mereka tidak punya body. Opsional, bisa diulang, dan tidak tergantung urutan.
Contoh utamanya: pagination
Bayangkan endpoint yang mengembalikan daftar buku berformat halaman. Panggilan pertama, /api/books, memakai bawaan server dan mengembalikan sepotong data plus metadata yang menggambarkan keseluruhan set:
{ "data": [ /* ...the first 20 books (the default page)... */ ], "total": 100, // how many books exist in total "currentPage": 1, // which page this is (default = 1) "limit": 20, // page size (default limit) "totalPages": 5 // 100 / 20 = 5 pages }
Klien memakai metadata respons ini untuk membuat request berikutnya. Bawaan-nya tidak butuh parameter; untuk ke halaman dua klien mengirim /api/books?page=2. Mekanisme yang sama membawa filter (mis. filter berdasar nilai yang ditentukan pengguna) dan urutan sort (naik atau turun), semuanya sebagai pasangan key/value di query.
Parameter Path vs Query
Keduanya menaruh data di URL, dan gampang tertukar, jadi pantas dipisahkan dengan presisi. Keduanya melayani tujuan berbeda.
Parameter path melayani satu tujuan khusus: ekspresi semantik resource mana, "pengguna yang ID-nya 123". Secara teknis Anda bisa mendorong nilai pencarian ke path (/api/search/some+value), tapi sulit dirawat dan meniadakan tujuan memberi route makna semantik yang jelas. Metadata seperti istilah pencarian pantasnya di query string: /api/search?query=some+value.
Route Bertingkat (Nested)
Nesting sebenarnya bukan jenis route yang terpisah, melainkan praktik yang akan Anda lihat di mana-mana. Di API bergaya REST, Anda sering menyarangkan resource berbeda untuk mengekspresikan hubungan semantik. Tiap kedalaman menambah menyempitkan makna dan memetakan ke handler-nya sendiri, mengembalikan granularitas data yang berbeda.
Menyusuri contoh terdalam, /api/users/123/posts/456:
- /api/users, bagian statis; sendirian ia mengembalikan daftar semua pengguna.
- 123, path parameter untuk ID pengguna; /api/users/123 mengembalikan pengguna itu satu.
- posts, bagian statis yang menamai sub-koleksi; /api/users/123/posts mengembalikan semua post pengguna itu.
- 456, path parameter untuk sebuah post spesifik; route lengkapnya mengambil "post dengan ID 456 milik pengguna dengan ID 123".
Itulah kenapa disebut route bertingkat: Anda menyarangkan berbagai jenis informasi di tingkat yang berbeda untuk mengekspresikan makna semantik yang berbeda, dan masing-masing menghasilkan respons yang berbeda. Praktik ini akan Anda lihat hampir di mana-mana begitu API punya kompleksitas sedang saja.
Siklus Hidup Routing
Merangkai semuanya, inilah jalur yang ditempuh satu request, langkah routing dalam konteksnya.
Versioning & Deprecation
Route versioning adalah praktik yang sangat umum untuk mengelola perubahan di endpoint API. Anda menaruh kata kunci versi di route, /api/v1/products dan /api/v2/products. Ia tampak seperti route biasa dengan segmen v1 atau v2 ditambahkan.
Kenapa repot? Anggap v1 mengembalikan tiap produk sebagai { id, name, price }. Belakangan muncul kebutuhan baru, misalnya Anda kini melayani aplikasi mobile dan harus mengubah bentuk respons, menamai ulang name menjadi title. Jika route yang hidup diubah, semua klien yang ada rusak. Versioning membuat kedua bentuk hidup berdampingan:
| /api/v1/products | /api/v2/products |
|---|---|
{ id, name, price } | { id, title, price } |
Ini melakukan dua hal. Ia mengekspresikan maksud dengan jelas, v1 dan v2 dari respons berdampingan. Dan artinya Anda tidak perlu mengubah seluruh route (tidak perlu /api/new-products); cukup menaikkan versi.
Mengumumkan bahwa sebuah endpoint (mis. v1) akan pensiun begitu v2 rilis. Para engineer mendapat jendela migrasi yang jelas untuk memindahkan request mereka ke v2; hanya setelah itu tim menghapus v1. Versioning ditambah deprecation memberi alur kerja yang stabil untuk mengirim perubahan yang memecah (breaking changes) tanpa memecah klien pada saat itu juga.
Route Catch-All
Route catch-all menangani request untuk route yang tidak benar-benar dilayani server. Anggap datang request untuk /api/v3/products tapi tidak ada handler-nya. Server menaruh catch-all paling akhir, setelah semua route dan method spesifik, biasanya ditulis dengan wildcard, /*. Request apa pun yang lolos dari semua kecocokan sebelumnya mendarat di sana.
Tanpa penanganan catch-all, perilaku bawaannya sering berupa respons null, yang membingungkan. Dengan itu, handler mengembalikan pesan ramah pengguna, "route ini tidak ada" / "route not found", jadi klien paham jelas bahwa server tidak melayani endpoint itu.
Wildcard * mencocokkan apa pun, persis itulah kenapa catch-all wajib didaftarkan paling akhir. Ditaruh di awal, ia bakal mencegat semua request sebelum route yang asli mendapat giliran. Aturan yang bisa diandalkan: route spesifik dulu, catch-all di paling ujung.
Sebuah Router di Go
Router, pada dasarnya, adalah masalah dispatch berorientasi objek kecil: sebuah tabel dari "method + route" ke handler. Go mengekspresikan empat pilar OOP lewat interface (abstraksi + polimorfisme), field tak-terekspor (enkapsulasi), dan struct embedding (komposisi, bentuk inheritance Go). Komentar ber-tag OOP menunjuk masing-masing.
package main import ("fmt"; "strings") // ABSTRACTION: Handler is an interface, it names a capability // ("be able to Handle a request") without saying how. Any type // with a Handle method IS-A Handler. The router depends only on // this contract, never on a concrete type. type Handler interface { Handle(req Request) Response } type Request struct { Method string Path string Params map[string]string // extracted path params (:id) Query map[string]string // query params (?page=2) } type Response struct { Status int; Body string } // ENCAPSULATION: "routes" is lowercase => unexported => private to // the package. Outsiders cannot touch the table directly; they go // through the methods we expose (Register / Dispatch). type Router struct { routes map[string]Handler // key = "METHOD /route" } func NewRouter() *Router { return &Router{routes: make(map[string]Handler)} } // Register binds method + route -> handler (the dispatch table). func (r *Router) Register(method, pattern string, h Handler) { r.routes[method+" "+pattern] = h // method + path = the unique key } // POLYMORPHISM: Dispatch calls h.Handle(req) without knowing which // concrete type h is. Same call, different behaviour per handler. func (r *Router) Dispatch(req Request) Response { for key, h := range r.routes { method, pattern, _ := strings.Cut(key, " ") if method == req.Method && match(pattern, req.Path, &req) { return h.Handle(req) // polymorphic dispatch } } return Response{404, "route not found"} // catch-all fallback } // match compares "/users/:id" against "/users/123", filling Params. func match(pattern, path string, req *Request) bool { p, q := strings.Split(pattern, "/"), strings.Split(path, "/") if len(p) != len(q) { return false } for i := range p { if strings.HasPrefix(p[i], ":") { // dynamic segment req.Params[p[i][1:]] = q[i] // bind :id => "123" } else if p[i] != q[i] { // static segment must match exactly return false } } return true } // "INHERITANCE" via COMPOSITION: BaseHandler holds shared logic; // concrete handlers EMBED it and reuse its methods. type BaseHandler struct{ Name string } func (b BaseHandler) log(req Request) { fmt.Printf("[%s] %s %s\n", b.Name, req.Method, req.Path) } // GetUser embeds BaseHandler (composition) and satisfies Handler. type GetUser struct{ BaseHandler } func (h GetUser) Handle(req Request) Response { h.log(req) // reused (inherited) behaviour return Response{200, "user id = " + req.Params["id"]} } type ListBooks struct{ BaseHandler } func (h ListBooks) Handle(req Request) Response { h.log(req) page := req.Query["page"] // query param drives pagination if page == "" { page = "1" } return Response{200, "books page " + page} } func main() { r := NewRouter() r.Register("GET", "/api/users/:id", GetUser{BaseHandler{"users"}}) r.Register("GET", "/api/books", ListBooks{BaseHandler{"books"}}) req := Request{Method: "GET", Path: "/api/users/123", Params: map[string]string{}, Query: map[string]string{}} fmt.Println(r.Dispatch(req).Body) // => user id = 123 }
Sebuah Router di Python
Arsitektur yang sama dalam sintaks kelas klasik: abstract base class untuk kontraknya (abstraksi), subclassing sungguhan (inheritance), method yang di-override (polimorfisme), dan atribut privat yang di-name-mangle (enkapsulasi).
from abc import ABC, abstractmethod from dataclasses import dataclass, field from urllib.parse import urlsplit, parse_qs # ABSTRACTION: Handler is an Abstract Base Class. @abstractmethod # forces every subclass to define handle(). You cannot create a # Handler directly, it is a pure contract. class Handler(ABC): @abstractmethod def handle(self, req: "Request") -> "Response": ... @dataclass class Request: method: str path: str params: dict = field(default_factory=dict) # path params (:id) query: dict = field(default_factory=dict) # query params (?page=2) @dataclass class Response: status: int body: str # INHERITANCE: BaseHandler is a concrete parent holding shared # behaviour (logging). Subclasses inherit and reuse _log(). class BaseHandler(Handler): def __init__(self, name): self._name = name # protected by convention def _log(self, req): print(f"[{self._name}] {req.method} {req.path}") # POLYMORPHISM: each subclass OVERRIDES handle() differently, yet # the router calls them all identically, handler.handle(req). class GetUser(BaseHandler): # IS-A BaseHandler IS-A Handler def handle(self, req): self._log(req) # inherited from parent return Response(200, f"user id = {req.params['id']}") class ListBooks(BaseHandler): def handle(self, req): self._log(req) page = req.query.get("page", ["1"])[0] # query param return Response(200, f"books page {page}") # ENCAPSULATION: self.__routes is name-mangled to _Router__routes, # so it is effectively private. The public surface is register() # and dispatch(); the table stays hidden. class Router: def __init__(self): self.__routes: dict[tuple[str, str], Handler] = {} def register(self, method, pattern, handler): self.__routes[(method, pattern)] = handler # method + path key def dispatch(self, method, url): parts = urlsplit(url) query = parse_qs(parts.query) # ?a=1&b=2 -> dict for (m, pattern), handler in self.__routes.items(): params = self.__match(pattern, parts.path) if m == method and params is not None: return handler.handle( Request(method, parts.path, params, query)) # polymorphic return Response(404, "route not found") # catch-all @staticmethod def __match(pattern, path): p = pattern.strip("/").split("/") q = path.strip("/").split("/") if len(p) != len(q): return None params = {} for seg, val in zip(p, q): if seg.startswith(":"): # dynamic segment params[seg[1:]] = val # bind :id -> "123" elif seg != val: # static must match exactly return None return params if __name__ == "__main__": r = Router() r.register("GET", "/api/users/:id", GetUser("users")) r.register("GET", "/api/books", ListBooks("books")) print(r.dispatch("GET", "/api/users/123").body) # user id = 123 print(r.dispatch("GET", "/api/books?page=2").body) # books page 2
Go meraihnya lewat interface + embedding; Python lewat ABC + pewarisan kelas. Keduanya adalah tabel dispatch privat ber-kunci method + route, kontrak handler abstrak, dispatch polimorfik, dan fallback catch-all, seluruh model routing dari manual ini, dalam kode.
Glosarium Routing
Setiap istilah routing yang dipakai di atas, dalam satu tempat.
| Istilah | Makna |
|---|---|
| Routing | Memetakan path URL (+ method HTTP) ke handler / logika sisi server. |
| Maksud / aksi | Apa yang ingin dilakukan request, ambil, tambah, perbarui, hapus (method HTTP). |
| Route / route path | Bagian alamat yang menyebut di mana di server, mis. /api/books. |
| Handler | Instruksi yang berjalan saat route cocok; menjalankan logika bisnis. |
| Resource | "Benda" yang disasar route, kata benda seperti users atau books. |
| Kunci unik | Method + route yang digabung; itulah kunci server mencari handler. |
| Route statis | Path tetap dan konstan tanpa parameter variabel. |
| Route dinamis | Route dengan parameter variabel di dalam path. |
| Parameter path | Nilai dinamis di dalam path (:id); mengidentifikasi resource mana. Disebut juga "route parameter". |
| Parameter query | Pasangan key=value setelah ?; metadata untuk filter/sort/paginate/cari. |
| Pagination | Mengembalikan daftar dalam halaman, digerakkan parameter query seperti page dan limit. |
| Route bertingkat | Hirarki segmen yang mengekspresikan kepemilikan, /users/123/posts/456. |
| Makna semantik | Route yang ditulis supaya terbaca seperti pernyataan jelas ramah manusia (tujuan routing REST). |
| Versioning | Menaruh /v1/, /v2/ di route untuk mengembangkan API dengan aman. |
| Deprecation | Menandai versi lama hendak pensiun; klien mendapat jendela migrasi. |
| Route catch-all | Route /* pilihan terakhir yang mengembalikan "route not found" yang ramah. |
| Wildcard | Token (*) yang mencocokkan apa pun; hidup di catch-all. |
Sebuah request menggabung method (apa) dan route (di mana). Server menyatukannya menjadi kunci unik, mencocokkannya ke tabelnya, route statis dulu, dinamis berikutnya, catch-all terakhir, mengekstrak path param dari route dan query param dari query string, lalu memanggil handler yang cocok. Beri versi pada route untuk berkembang, deprecate untuk pensiun, dan catch-all untuk gagal dengan sopan.