Handler, Service & Repository.
Penelusuran dari prinsip dasar tentang perjalanan satu request HTTP di dalam server Anda: lewat routing, tiga lapisan arsitektur, rantai middleware, dan request context yang mengikat semuanya. Implementasi lengkap di Go dan Python ditampilkan berdampingan.
01 Klien & Server
Semuanya bermula dari dua peserta yang bicara lewat HTTP: klien (browser, aplikasi mobile, layanan lain) dan server. Klien mengirim request; server akhirnya mengirim balik respons.
Kita sudah mempelajari siklus hidup yang eksternal terhadap server, bagaimana request dikemas, dikirim melewati jaringan, dan bagaimana respons kembali. Yang belum kita telusuri adalah apa yang terjadi di dalam server: rangkaian panjang peristiwa antara sesaat request tiba dan sesaat respons keluar. Rangkaian internal itulah subjek manual ini.
02 Siklus Hidup Request di Dalam Server
Dari saat request tiba di server sampai saat respons dikembalikan, banyak yang terjadi. Kita sebut ini request lifecycle. Untuk memahami arsitekturnya, bantu menelusuri request dari atas ke bawah, mengikutinya dalam urutan persis prosesnya: kedatangan, routing, handler, service, repository, dan akhirnya respons dalam perjalanan keluar.
Urutan atas-ke-bawah ini disengaja. Setiap topik dibangun di atas yang terakhir, mengikuti jalur yang secara fisik ditempuh data melewati kode Anda.
03 Titik Masuk & Routing
Saat request tiba, sistem operasi meneruskan request HTTP ke port mana pun tempat server Anda mendengarkan, :3000, :4000, atau port apa pun yang Anda konfigurasi. Titik tempat server Anda pertama menerima request itu adalah entry point-nya. Server Anda selalu mendengarkan di port itu, dan mendengarkan itulah cara ia menangkap request yang masuk.
Routing
Tepat setelah entry point datang routing. Server mengekspos banyak route, /users, /users/123, route dinamis seperti /users/:id, dan seterusnya. Algoritma routing memeriksa method dan path request yang masuk lalu memetakannya ke handler tertentu: fungsi yang sudah didefinisikan untuk route itu.
Handler (juga disebut controller) adalah fungsi apa pun yang Anda definisikan untuk menangani request bagi sebuah route. Seluruh kerja routing adalah memutuskan handler mana yang berjalan untuk request masuk yang mana.
04 Kenapa Tiga Lapisan?
Begitu routing memilih handler, kita menjumpai struktur tiga bagian di jantung backend yang tertata rapi: handler (controller), service, dan repository. Pertanyaan yang wajar: kenapa memecahnya menjadi tiga komponen alih-alih menjejalkan semua logika ke dalam satu handler?
Jawaban jujurnya: tidak ada keharusan untuk memisahkannya. Anda bisa melakukan semuanya di satu handler. Pemisahan adalah pola desain: pilihan yang membelikan Anda manfaat nyata:
- Codebase Anda jadi skalabel saat tumbuh.
- Lebih mudah dirawat seiring waktu.
- Lebih mudah menambah fitur tanpa merusak bagian yang tak berhubungan.
- Lebih mudah di-debug karena tiap lapisan punya satu tanggung jawab yang jelas.
Inilah prinsip separation of concerns. Sisa Bagian II menapaki tiap lapisan dalam urutan request menjumpainya.
05 Lapisan Controller / Handler
Handler adalah titik masuk untuk sebuah route setelah routing mencocokkannya. Tugas besarnya adalah mengendalikan alur data: dari klien ke dalam server, dan dari server kembali ke klien. Yang penting, di hampir semua framework dan bahasa, handler Anda diberi dua hal oleh runtime itu sendiri.
Membawa semua yang dikirim klien: method, path, header, parameter query, body. Anda tidak membuatnya; Go, Express, Flask, dll. menyerahkannya ke Anda di tiap request.
Pegangan tempat Anda men-set status code, memodifikasi header respons, dan mengirim body kembali. Juga disediakan otomatis; Anda menerimanya, tidak menyusunnya.
Dengan dua objek itu di tangan, handler menjalankan alur kerja yang presisi dan berurutan.
- Ekstraksi Data. Ambil data yang relevan dari objek request. Apa yang diambil tergantung method: GET → parameter query; POST / PUT / PATCH → body request; DELETE → biasanya tidak ada, kadang body.
- Binding (Deserialisasi). Body tiba sebagai string JSON karena JSON format yang bisa diserialkan dan bepergian dengan baik melewati batas front-end/back-end. Handler men-deserialize-nya ke tipe natif bahasa, struct Go, dict/kelas Python, struct Rust. Framework menyebut ini binding. Jika gagal, berhenti seketika dan kembalikan 400 Bad Request.
- Validasi. Konfirmasi data cocok dengan bentuk yang diharapkan: field wajib ada, tipe benar, tidak ada payload jahat. Validasi semuanya dari klien eksternal: path param, query param, dan body, dan se-spesifik mungkin.
- Transformasi. Opsional, bentuk ulang data yang sudah divalidasi demi kenyamanan lapisan di bawahnya, misalnya menyuntikkan nilai default.
- Delegasi. Serahkan data yang bersih, tervalidasi, dan tertransformasi (plus konteks seperti ID pengguna yang terautentikasi) turun ke lapisan Service.
- Mengirim Respons. Saat service mengembalikan, pilih status code yang benar dan kirim respons akhir kembali ke klien.
Catatan soal Node.js vs Go & Python
Di aplikasi Node.js/Express, langkah deserialisasi sering terjadi di hulu dalam middleware (body parser json()), jadi JSON sudah berupa objek JavaScript saat handler Anda berjalan. Di Go dan Python Anda biasanya men-deserialize secara eksplisit di dalam handler, ke struct, dictionary, atau kelas.
Langkah 1-2: Ekstrak & bind body; 400 saat gagal
// CreateBookRequest is our native format, the bind target. type CreateBookRequest struct { Title string `json:"title"` Author string `json:"author"` } func CreateBookHandler(w http.ResponseWriter, r *http.Request) { var req CreateBookRequest // Step 1 + 2: extract the body and deserialize (bind) into the struct. if err := json.NewDecoder(r.Body).Decode(&req); err != nil { // Deserialization failed -> the payload is malformed. http.Error(w, "invalid request body", http.StatusBadRequest) // 400 return // terminate the request here; do not proceed. } // ... validation, transformation, delegation follow ... }
from dataclasses import dataclass from flask import request, jsonify @dataclass class CreateBookRequest: title: str author: str def create_book_handler(): # Step 1 + 2: extract body and deserialize into a native dict/class. payload = request.get_json(silent=True) if payload is None: # Deserialization failed -> malformed payload. return jsonify({"error": "invalid request body"}), 400 req = CreateBookRequest(**payload) # ... validation, transformation, delegation follow ...
Langkah 3-4: Validasi, lalu transformasi (suntik default untuk query param opsional)
Aturan desain API yang baik: buat parameter query opsional sedapat mungkin. Anggap GET /books?sort=name|date. Jika klien tidak mengirim apa pun, validasi harus tetap lulus, jadi langkah transformasi menyuntikkan default yang masuk akal.
func ListBooksHandler(w http.ResponseWriter, r *http.Request) { sort := r.URL.Query().Get("sort") // "" if absent // VALIDATION: if present, it must be one of the allowed values. if sort != "" && sort != "name" && sort != "date" { http.Error(w, "sort must be 'name' or 'date'", http.StatusBadRequest) return } // TRANSFORMATION: query params are optional -> inject a default. if sort == "" { sort = "date" // downstream layers never see an empty value } books, err := bookService.ListBooks(r.Context(), sort) // delegate if err != nil { http.Error(w, "could not fetch books", http.StatusInternalServerError) // 500 return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(books) // 200 with the array of books }
def list_books_handler(): sort = request.args.get("sort") # None if absent # VALIDATION: if present, must be an allowed value. if sort is not None and sort not in ("name", "date"): return jsonify({"error": "sort must be 'name' or 'date'"}), 400 # TRANSFORMATION: optional param -> inject a default. if sort is None: sort = "date" try: books = book_service.list_books(sort) # delegate except Exception: return jsonify({"error": "could not fetch books"}), 500 return jsonify(books), 200 # array of books
06 Lapisan Service
Lapisan service adalah tempat pemrosesan sebenarnya: logika bisnisnya, terjadi. Satu aturan terpenting mengaturnya:
Lapisan service tidak boleh tahu sedikit pun soal HTTP. Tanpa objek request/response, tanpa status code, tanpa urusan validasi. Jika Anda melirik sebuah method service, Anda tidak seharusnya bisa tahu ia dipakai di API. Ia cuma fungsi: data masuk, pemrosesan, data keluar.
Isolasi inilah yang menjaga sistem tetap terpisah (decoupled). Service memutuskan apa yang dilakukan; handler memutuskan bagaimana melaporkannya lewat HTTP. Di situlah garis tanggung jawab kita tarik.
Orkestrasi
Satu method service bisa berbuat banyak. Ia bisa memanggil satu atau beberapa method repository dan menggabungkan hasilnya, membuat panggilan API eksternal, mengirim email, memicu notifikasi, apa pun yang dibutuhkan operasi bisnis. Peran koordinasi ini disebut orkestrasi: service menjahit data dari banyak sumber dan mengembalikan satu hasil bersih ke handler. Service yang hanya mengirim email mungkin tidak pernah menyentuh repository sama sekali.
// Notice: no http.Request, no ResponseWriter, no status codes. // You cannot tell from this signature that it serves an API. func (s *BookService) ListBooks(ctx context.Context, sort string) ([]Book, error) { // Orchestration: call the repository for the data it needs. books, err := s.repo.FindAllBooks(ctx, sort) if err != nil { return nil, err // bubble the error up; the handler decides the code } // Could also: enrich, merge other repo calls, send notifications... return books, nil } // A service that needs no database at all is perfectly valid: func (s *BookService) NotifyOwner(email string) error { return s.mailer.Send(email, "Your book was added") }
class BookService: def __init__(self, repo, mailer): self.repo = repo self.mailer = mailer # No request, no response, no status codes, just logic. def list_books(self, sort: str) -> list: # Orchestration: ask the repository for what it needs. books = self.repo.find_all_books(sort) # Could merge other repo calls, enrich, notify, etc. return books # A purely-logic service that never touches the DB: def notify_owner(self, email: str) -> None: self.mailer.send(email, "Your book was added")
07 Lapisan Repository
Repository (atau lapisan database) punya satu perhatian: bicara ke database. Ia menerima data dari service, menyusun query database: untuk insert, filter, atau sortir, menjalankannya, dan mengembalikan hasil mentah ke atas kepada service.
Method repository harus melakukan satu hal dan mengembalikan satu jenis data. Jangan menulis satu method yang, tergantung parameter opsional, mengembalikan satu buku atau semua buku. Tulis dua method terpisah: FindAllBooks() dan FindBookByID(id).
// ONE method, ONE shape of result: all books, sorted. func (r *BookRepo) FindAllBooks(ctx context.Context, sort string) ([]Book, error) { // Build the query from the data the service handed down. query := fmt.Sprintf("SELECT id, title, author FROM books ORDER BY %s", sort) rows, err := r.db.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() var books []Book for rows.Next() { var b Book rows.Scan(&b.ID, &b.Title, &b.Author) books = append(books, b) } return books, nil } // A SEPARATE method for the single-book case. No optional toggles. func (r *BookRepo) FindBookByID(ctx context.Context, id int) (Book, error) { var b Book err := r.db.QueryRowContext(ctx, "SELECT id, title, author FROM books WHERE id = $1", id). Scan(&b.ID, &b.Title, &b.Author) return b, err }
class BookRepo: def __init__(self, db): self.db = db # ONE method, ONE result shape: all books, sorted. def find_all_books(self, sort: str) -> list: # Build the query from data passed down by the service. query = f"SELECT id, title, author FROM books ORDER BY {sort}" cur = self.db.execute(query) return [dict(row) for row in cur.fetchall()] # SEPARATE method for one book. No optional toggle parameter. def find_book_by_id(self, book_id: int) -> dict: cur = self.db.execute( "SELECT id, title, author FROM books WHERE id = ?", (book_id,)) return dict(cur.fetchone())
08 Siklus Hidup Lengkap, End to End
Merangkai lapisan-lapisannya, inilah round trip lengkap untuk satu panggilan API, mis. GET /books:
- Binding. Handler mengekstrak data dari objek request dan men-deserialize-nya ke format natif.
- Validasi & Transformasi. Handler memvalidasi data dan menyuntikkan default / membentuk ulang sesuai kebutuhan.
- Panggilan service. Handler mendelegasikan ke service, yang melakukan pemrosesan sungguhan, memanggil repository untuk kerja database apa pun.
- Hasil kembali. Repository mengembalikan baris ke service; service mengembalikan hasil bersih ke handler.
- ResponseThe handler picks the status code and sends the response. 200 / 201 / 204 on success; 400 for client error; 500 for server error.
09 Apa Itu Middleware
Sekarang putar ulang siklus hidupnya, tapi perhatikan celahnya. Antara entry point dan routing, antara routing dan handler, dan antara handler dan respons, ada batas-batas: titik tempat fungsi ekstra bisa berjalan. Fungsi yang berjalan di celah-celah itu adalah middleware. Namanya harfiah: mereka dieksekusi di tengah konteks eksekusi lainnya.
Middleware opsional. Bisa banyak, bisa tidak ada sama sekali; request bisa mengalir lurus dari routing ke handler. Anda menambahkannya hanya saat ada kebutuhan yang memintanya.
Middleware pada dasarnya jenis handler khusus. Seperti handler biasa ia menerima objek request dan objek response dari runtime, jadi ia juga bisa membaca nilai dari request, memodifikasi header respons, bahkan mengirim respons langsung ke klien. Tapi ia menerima hal ketiga juga, yang dijelaskan Bagian 10.
10 Fungsi next()
Hal ketiga yang diterima middleware adalah next, sebuah fungsi. Memanggil next() menyerahkan eksekusi ke konteks berikutnya: entah middleware berikutnya, langkah routing, atau handler akhir. Begitulah request menyeberang dari satu batas ke batas berikutnya.
(1) Lakukan kerjanya, lalu panggil next() untuk melanjutkan rantai. (2) Tidak memanggil next() dan sebagai gantinya langsung mengirim respons: short-circuit request sebelum ia pernah mencapai handler. Keluar lebih awal inilah persis kenapa cek auth yang gagal atau rate limit yang tersandung bisa menghentikan request dengan murah.
Karena middleware punya objek request dan response, ia bisa membaca dan memodifikasi request, dan bisa menghentikan request tepat di tempatnya dengan mengembalikan respons. Itulah seluruh batas kekuatan middleware.
// In Go, middleware wraps the "next" handler. Calling next.ServeHTTP // is the equivalent of next(): pass execution along the chain. func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s", r.Method, r.URL.Path) // do work // EARLY EXIT example (short-circuit, never calls next): if r.Header.Get("X-Blocked") == "yes" { http.Error(w, "forbidden", http.StatusForbidden) return // request stops here } next.ServeHTTP(w, r) // === next(): continue the chain === }) }
# A WSGI/Flask-style middleware receives the request and a `next` # callable that invokes the rest of the chain. def logging_middleware(next): def wrapper(request): print(f"{request.method} {request.path}") # do work # EARLY EXIT example (short-circuit, never calls next): if request.headers.get("X-Blocked") == "yes": return Response("forbidden", status=403) # stops here return next(request) # === next(): continue the chain === return wrapper
Kenapa pakai middleware sama sekali?
Alasan yang sama kita memakai fungsi: menghindari mengulang baris yang sama di mana-mana. Backend bisa melayani ribuan atau jutaan request dan mengekspos ratusan endpoint. Operasi umum, keamanan, logging, autentikasi, parsing, kompresi, perlu berjalan untuk setiap request. Tanpa middleware Anda harus menduplikasi kode itu di tiap handler. Bahkan mengekstrak fungsi pembantu tetap memaksa Anda memanggilnya di tiap handler. Middleware memusatkan logika umum dan menerapkannya otomatis di sepanjang rantai.
11 Kenapa Urutan Penting
Setiap middleware memakai next() untuk menyerahkan eksekusi ke yang sesudahnya, jadi request mengalir melewatinya secara sekuensial, dalam urutan Anda mendaftarkannya. Urutan itu bukan hiasan, ia mengubah perilaku.
Contoh paling tajam adalah penanganan error global, yang biasanya ditaruh paling akhir. Karena request mengalir dalam satu arah, middleware hanya bisa menangkap error yang berasal di hulu darinya. Jika Anda menaruh error handler di tengah, error yang dilempar belakangan di handler akan mengalir melewatinyanya dan tak pernah tertangkap. Letakkan paling akhir dan ia bisa menangkap error dari mana pun di awalnya.
CORS dulu (tolak origin asing sedini mungkin) → logging → autentikasi → pemeriksaan khusus aplikasi → handler → penanganan error global paling akhir. Luangkan waktu sungguhan memutuskan urutan ini untuk aplikasi Anda.
12 Middleware yang Umum
Masing-masing berikut memenuhi syarat middleware karena dua alasan yang sama: ia harus berjalan untuk setiap request, dan ia perlu membaca request dan/atau memodifikasi respons.
Browser menegakkan same-origin policy: aplikasi web di example.com hanya boleh mengakses resource dari example.com kecuali server jarak jauh mengirim header yang benar. Middleware CORS membaca origin request (disediakan otomatis oleh runtime); jika cocok dengan origin frontend yang diizinkan, ia menambah header respons yang sesuai supaya browser tidak memblokir respons, lalu memanggil next(). Jika tidak, ia mengabaikan header itu dan browser memblokirnya sebagai bawaan. Ditaruh di awal.
Men-set header seperti Content-Security-Policy di tiap respons, lalu meneruskan ke middleware berikutnya.
Mengekstrak token (JWT, session ID, dll.) dari header atau payload dan memverifikasinya. Saat gagal, ia mengembalikan 401 Unauthorized seketika dan menghentikan request, tanpa handler, tanpa middleware lanjutan. Saat sukses, ia mengekstrak detail pengguna (user ID, role, izin) dan menyimpannya di request context sebelum memanggil next().
Melacak berapa banyak request yang dibuat sebuah IP dalam jendela yang Anda tetapkan (mis. 30 panggilan dalam 2 detik). Lewat ambang, ia mengembalikan 429 Too Many Requests untuk melindungi resource server; di bawahnya, ia memanggil next().
Mencatat detail request, path, method, parameter query, body, ke terminal atau file log untuk debugging, pelaporan, dan audit.
Menangkap error tak terstruktur apa pun dari hulu mana pun, memutuskan apakah itu error klien (4xx) atau server (5xx), dan mengembalikan pesan bersih terstruktur (sering dengan pesan dan kode error yang bisa dipetakan frontend ke pesan ramah). Ditaruh paling akhir.
Mengompresi respons besar (mis. gzip) supaya bepergian efisien; browser modern mendekompresinya secara transparan kembali ke format yang bisa dipakai.
Serialisasi/deserialisasi, bahkan validasi/transformasi, bisa didelegasikan ke middleware (mis. body parser) supaya handler tidak mengulanginya. Persis inilah kenapa body parser json() Node membuat JSON sudah berupa objek JS saat handler berjalan.
Middleware CORS & Autentikasi dalam kode
var allowedOrigin = "https://app.example.com" func CORSMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") // runtime gives us this if origin == allowedOrigin { w.Header().Set("Access-Control-Allow-Origin", origin) } next.ServeHTTP(w, r) // pass along; browser blocks if header absent }) } func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") userID, role, err := verifyToken(token) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) // 401, stop return } // SUCCESS: stash identity in the request context, then continue. ctx := context.WithValue(r.Context(), "userID", userID) ctx = context.WithValue(ctx, "role", role) next.ServeHTTP(w, r.WithContext(ctx)) }) }
ALLOWED_ORIGIN = "https://app.example.com" def cors_middleware(next): def wrapper(request): resp = next(request) origin = request.headers.get("Origin") # runtime gives us this if origin == ALLOWED_ORIGIN: resp.headers["Access-Control-Allow-Origin"] = origin return resp return wrapper def auth_middleware(next): def wrapper(request): token = request.headers.get("Authorization") try: user_id, role = verify_token(token) except Exception: return Response("unauthorized", status=401) # stop # SUCCESS: stash identity in the request context, then continue. request.context["user_id"] = user_id request.context["role"] = role return next(request) return wrapper
13 Apa Itu Request Context
Middleware auth tadi melakukan sesuatu yang halus: ia menyimpan user ID dan role di suatu tempat supaya handler berikutnya bisa membacanya. "Suatu tempat" itu adalah request context.
Sepotong penyimpanan atau state: biasanya key–value store sederhana, yang ter-scope ke satu request HTTP. Setiap request mendapat konteksnya sendiri; ia tidak bocor ke request lain. Setiap framework di setiap bahasa (Node, Go, Python) mengirimkan implementasi konsep ini; mekanikanya berbeda, gagasannya sama.
Kenapa ia ada? Karena request melewati banyak batas fungsi terisolasi: CORS, logging, routing, auth, pemeriksaan izin, handler, error handler. Konteks memberi mereka semua satu tempat bersama untuk membaca dan menulis state tanpa menggandeng erat (tight coupling), tanpa satu middleware harus menyerahkan nilai ke berikutnya secara manual.
14 Kasus Pakai / Meneruskan Data Autentikasi
Kasus kanonisnya. Middleware auth memverifikasi kredensial, mengekstrak userid dan role, dan menuliskannya ke konteks. Jauh di hilir, handler POST /books perlu menstempel buku baru dengan pemiliknya. Alih-alih memercayai userid yang dikirim di payload JSON klien, ia membaca ID dari konteks.
Jika Anda mengambil userid dari payload klien, klien jahat bisa mengirim ID orang lain_ dan bertindak sebagai mereka. Membaca identitas dari konteks, yang hanya diisi middleware auth terverifikasi Anda, menutup lubang itu. Role yang tersimpan di konteks yang sama menggerakkan pemeriksaan izin (apakah pengguna ini admin? apakah ia punya akses tulis?).
func CreateBookHandler(w http.ResponseWriter, r *http.Request) { var req CreateBookRequest json.NewDecoder(r.Body).Decode(&req) // Read the trusted user ID FROM THE CONTEXT, not from req. // The auth middleware put it there after verifying the token. userID := r.Context().Value("userID").(int) role := r.Context().Value("role").(string) if role != "admin" && role != "user" { http.Error(w, "forbidden", http.StatusForbidden) return } // Persist with the SERVER-VERIFIED owner id, never the client's. book, _ := bookService.Create(r.Context(), req, userID) w.WriteHeader(http.StatusCreated) // 201 json.NewEncoder(w).Encode(book) }
def create_book_handler(request): req = CreateBookRequest(**request.get_json()) # Read the trusted user id FROM THE CONTEXT, not from the body. # The auth middleware put it there after verifying the token. user_id = request.context["user_id"] role = request.context["role"] if role not in ("admin", "user"): return Response("forbidden", status=403) # Persist with the SERVER-VERIFIED owner id, never the client's. book = book_service.create(req, user_id) return jsonify(book), 201
15 Kasus Pakai / Tracing & Pembatalan
Tracing request
Middleware di awal bisa membuat ID unik (UUID) dan menyimpannya di konteks. Untuk seluruh siklus hidup request itu, setiap baris log bisa menyertakan ID ini, dan panggilan keluar ke microservice lain bisa meneruskannya di header seperti X-Request-ID. Saat Anda kemudian mengaudit log untuk men-debug masalah, ID tunggal itu memungkinkan Anda menelusuri satu request melewati setiap layanan yang disentuhnya: dari mana ia mulai dan ke mana-mana ia pergi.
func RequestIDMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := uuid.NewString() // one unique id for this request ctx := context.WithValue(r.Context(), "requestID", id) w.Header().Set("X-Request-ID", id) // echo it back / forward it log.Printf("[%s] %s %s", id, r.Method, r.URL.Path) next.ServeHTTP(w, r.WithContext(ctx)) }) }
import uuid def request_id_middleware(next): def wrapper(request): rid = str(uuid.uuid4()) # one unique id for this request request.context["request_id"] = rid print(f"[{rid}] {request.method} {request.path}") resp = next(request) resp.headers["X-Request-ID"] = rid # echo it back / forward it return resp return wrapper
Pembatalan, sinyal abort & deadline
Request context juga tempat standar untuk membawa sinyal pembatalan dan deadline. Jika klien putus atau deadline lewat, sinyal itu menyebar ke panggilan eksternal di hilir, jadi service tidak pernah menggantung selamanya menunggu kerja yang tidak lagi penting. Di Go ini context.Context bawaan dengan WithTimeout; di Python muncul sebagai timeout / cancellation token yang diteruskan sepanjang rantai panggilan.
Request masuk di entry point, mengalir lewat rantai middleware dan routing yang berurutan, mendarat di handler yang memvalidasi dan mentransformasi data, mendelegasikan ke service yang mengorkestrasi logika bisnis, yang memanggil repository untuk kerja database, semuanya sambil konteks per-request membawa identitas, trace ID, dan deadline di sisinya. Hasilnya naik kembali, dan handler memilih status code lalu mengirim respons. Itulah siklus hidup request lengkap.