|
| 1 | +--- |
| 2 | +description: "Pilih validator yang tepat per request saat satu prefix menampung beberapa method, pola selectValidator." |
| 3 | +--- |
| 4 | + |
| 5 | +# Pola Lanjutan |
| 6 | + |
| 7 | +Validator prefix berjalan untuk setiap method dan setiap path bersarang di bawah prefix itu. Itu baik saat satu schema cocok untuk seluruh prefix, tetapi sebuah resource nyata sering mencampur beberapa bentuk di bawah satu path. Halaman ini membahas pola yang memilih schema yang tepat per request. |
| 8 | + |
| 9 | +## Validator Berjalan Sebelum Routing |
| 10 | + |
| 11 | +Middleware berjalan sebelum router mencocokkan method atau path, jadi validator prefix menyala pada setiap request yang disentuh prefix, bahkan yang tidak dilayani handler mana pun. Sebuah validator di `/accounts` berjalan untuk `POST /accounts` dan untuk `GET /accounts/anything`, keduanya sebelum router memutuskan tidak ada rute seperti itu. |
| 12 | + |
| 13 | +Saat validator itu gagal, 422-nya mencapai client lebih dulu dan menyembunyikan status yang akan diproduksi router: |
| 14 | + |
| 15 | +- `POST /accounts` dengan header yang hilang mengembalikan **422**, bukan **405** yang akan diberikan handler POST yang tidak ada. |
| 16 | +- `GET /accounts/missing` dengan header yang hilang mengembalikan **422**, bukan **404** untuk path tak dikenal. |
| 17 | + |
| 18 | +Membatasi validator berdasarkan method dan path menjaga validasi pada request yang menjadi haknya dan membiarkan router menjawab sisanya. Dengan batas yang tepat, `POST /transfers/tx_abc123` mengembalikan **405** yang bersih alih-alih 422 validasi body, karena validator melewati request yang memang bukan tugasnya. |
| 19 | + |
| 20 | +## Satu Prefix, Beberapa Bentuk |
| 21 | + |
| 22 | +`router.use('/transfers', ...)` mencocokkan `/transfers` dan setiap path yang berlanjut dengan garis miring, seperti `/transfers/tx_abc123`. Aturan pencocokan berasal dari [Middleware Spesifik Rute](/id/middleware/route-specific). Sebuah resource `transfers` biasanya membawa dua request berbeda di bawah satu prefix itu: |
| 23 | + |
| 24 | +- `POST /transfers` mengirim body JSON yang butuh kontrak `json`. |
| 25 | +- `GET /transfers/:id` tidak membawa body dan memvalidasi param-nya di dalam handler. |
| 26 | + |
| 27 | +Mendaftarkan validator `json` pada seluruh prefix akan menjalankannya pada GET juga, dan membaca body yang tidak ada mengubah request valid menjadi kegagalan. Validator perlu menyala hanya untuk POST. |
| 28 | + |
| 29 | +## Helper selectValidator |
| 30 | + |
| 31 | +Sebuah pembungkus kecil menyelesaikannya. Helper ini menerima sebuah pemilih yang mengembalikan schema untuk request saat ini atau `undefined` untuk melewati, membangun validator sesuai kebutuhan, dan menyimpannya di cache agar setiap schema dibungkus sekali: |
| 32 | + |
| 33 | + |
| 34 | + |
| 35 | +```typescript twoslash |
| 36 | +import { type Context, type MiddlewareFn, Mware, type ValidationSchema } from '@neabyte/deserve' |
| 37 | + |
| 38 | +// Pilih sebuah schema atau lewati validasi |
| 39 | +function selectValidator(pick: (ctx: Context) => ValidationSchema | undefined): MiddlewareFn { |
| 40 | + const cache = new Map<ValidationSchema, MiddlewareFn>() |
| 41 | + return async (ctx: Context, next) => { |
| 42 | + const schema = pick(ctx) |
| 43 | + if (schema === undefined) { |
| 44 | + return await next() |
| 45 | + } |
| 46 | + let validator = cache.get(schema) |
| 47 | + if (validator === undefined) { |
| 48 | + // Bangun sekali, pakai ulang nanti |
| 49 | + validator = Mware.validator(schema) |
| 50 | + cache.set(schema, validator) |
| 51 | + } |
| 52 | + return await validator(ctx, next) |
| 53 | + } |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +Mengembalikan `undefined` langsung memanggil `next`, jadi request mengalir lewat tanpa disentuh. Mengembalikan sebuah schema menjalankan [Middleware Validator](/id/middleware/validation/validator-middleware) yang cocok sebelum handler. |
| 58 | + |
| 59 | +## Menghubungkan Ke Sebuah Prefix |
| 60 | + |
| 61 | +Pemilih membaca `ctx.pathname` dan method request untuk memutuskan. Di sini kontrak `json` berjalan hanya untuk POST koleksi, dan GET diteruskan untuk memvalidasi param-nya di handler: |
| 62 | + |
| 63 | +```typescript twoslash |
| 64 | +import { type Context, type MiddlewareFn, Define, Mware, Router, type ValidationSchema } from '@neabyte/deserve' |
| 65 | + |
| 66 | +declare function selectValidator(pick: (ctx: Context) => ValidationSchema | undefined): MiddlewareFn |
| 67 | + |
| 68 | +const router = new Router({ routesDir: './routes' }) |
| 69 | + |
| 70 | +const createTransfer = { |
| 71 | + json: Define((body: { amount: number }) => ({ amount: body.amount })) |
| 72 | +} |
| 73 | +// ---cut--- |
| 74 | +// Validasi body hanya pada POST koleksi |
| 75 | +router.use( |
| 76 | + '/transfers', |
| 77 | + selectValidator((ctx) => |
| 78 | + ctx.pathname === '/transfers' && ctx.request.method === 'POST' |
| 79 | + ? createTransfer |
| 80 | + : undefined |
| 81 | + ) |
| 82 | +) |
| 83 | +``` |
| 84 | + |
| 85 | +Handler `GET /transfers/:id` lalu memvalidasi param-nya sendiri dengan `Validator.check`, pendekatan dari [Membaca Data Tervalidasi](/id/middleware/validation/reading-data#memeriksa-params-di-handler). Validasi body dan validasi param tetap terpisah, masing-masing menyala hanya di tempat yang sesuai. |
| 86 | + |
| 87 | +## Memilih Di Antara Beberapa Schema |
| 88 | + |
| 89 | +Pemilih yang sama menangani lebih dari satu cabang saat sebuah prefix menampung banyak method. Setiap cabang mengembalikan schema untuk kasus itu, dan apa pun yang tidak cocok mengembalikan `undefined`: |
| 90 | + |
| 91 | +```typescript twoslash |
| 92 | +import { type Context, type MiddlewareFn, Define, Router, type ValidationSchema } from '@neabyte/deserve' |
| 93 | + |
| 94 | +declare function selectValidator(pick: (ctx: Context) => ValidationSchema | undefined): MiddlewareFn |
| 95 | + |
| 96 | +const router = new Router({ routesDir: './routes' }) |
| 97 | + |
| 98 | +const listQuery = { query: Define((q: Record<string, string>) => ({ page: Number(q['page'] ?? '1') })) } |
| 99 | +const createBody = { json: Define((body: { name: string }) => ({ name: body.name.trim() })) } |
| 100 | +// ---cut--- |
| 101 | +// Satu pemilih, satu schema per method |
| 102 | +router.use( |
| 103 | + '/users', |
| 104 | + selectValidator((ctx) => { |
| 105 | + const isCollection = ctx.pathname === '/users' |
| 106 | + if (isCollection && ctx.request.method === 'GET') { |
| 107 | + return listQuery |
| 108 | + } |
| 109 | + if (isCollection && ctx.request.method === 'POST') { |
| 110 | + return createBody |
| 111 | + } |
| 112 | + return undefined |
| 113 | + }) |
| 114 | +) |
| 115 | +``` |
| 116 | + |
| 117 | +Ini menjaga satu pendaftaran validator per prefix sementara setiap method mendapat schema persis yang dibutuhkannya. |
| 118 | + |
| 119 | +## Urutan Validasi |
| 120 | + |
| 121 | +Mengetahui apa yang gagal lebih dulu membuat 422 dapat diprediksi. Dua aturan mencakup setiap kasus, satu untuk sumber dan satu untuk guard. |
| 122 | + |
| 123 | +Sebuah schema dengan beberapa sumber memvalidasinya dalam urutan kemunculan key, dan sumber pertama yang gagal menghentikan sisanya. Sebuah schema `{ query, headers, cookies }` dengan query buruk dan header yang hilang hanya melaporkan alasan query, karena `query` datang lebih dulu dan kontrak header tidak pernah berjalan: |
| 124 | + |
| 125 | + |
| 126 | + |
| 127 | +```typescript twoslash |
| 128 | +import { Define } from '@neabyte/deserve' |
| 129 | + |
| 130 | +// Sumber divalidasi dalam urutan key |
| 131 | +const listAccounts = { |
| 132 | + query: Define((q: Record<string, string>) => q), |
| 133 | + headers: Define((h: Record<string, string>) => h), |
| 134 | + cookies: Define((c: Record<string, string>) => c) |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +Di dalam satu sumber, kontrak menentukan seberapa banyak yang dilaporkan. Satu guard yang mendorong ke array alasan memunculkan setiap field rusak sekaligus, sementara daftar guard berhenti pada kegagalan pertama. Pembagian itu datang langsung dari [Define Schema](/id/middleware/validation/define-schema#menyusun-beberapa-guard), jadi sebuah guard bentuk bisa melaporkan semua field yang hilang sementara guard invariant berikutnya hanya berjalan setelah bentuknya terpenuhi. |
| 139 | + |
| 140 | +Hasilnya terbaca rapi. Di antara sumber, kegagalan pertama menang, di dalam satu sumber kontrak memilih satu alasan atau banyak, dan di antara guard, guard pertama yang gagal menang. |
| 141 | + |
| 142 | +## Menyusun Struktur Schema |
| 143 | + |
| 144 | +Kontrak tidak harus berada di samping rute yang memakainya. Saat sebuah proyek tumbuh, folder tersendiri menjaga tiap kontrak tetap kecil dan membiarkan beberapa rute berbagi aturan yang sama. Sebuah tata letak yang berskala biasanya terlihat seperti ini: |
| 145 | + |
| 146 | +``` |
| 147 | +schemas/ |
| 148 | + _shared.ts # helper guard kecil dipakai lintas kontrak |
| 149 | + transfer.ts # satu resource, kontraknya |
| 150 | + account.ts |
| 151 | + index.ts # barrel yang mengelompokkan kontrak ke schema |
| 152 | +routes/ |
| 153 | + transfers.ts |
| 154 | + accounts.ts |
| 155 | +``` |
| 156 | + |
| 157 | +Barrel mengelompokkan kontrak tunggal menjadi schema per sumber yang dibaca sebuah rute, jadi perakitannya tetap di satu tempat: |
| 158 | + |
| 159 | +```typescript twoslash |
| 160 | +import { Define } from '@neabyte/deserve' |
| 161 | +declare const Transfer: ReturnType<typeof Define> |
| 162 | +declare const AccountQuery: ReturnType<typeof Define> |
| 163 | +declare const ApiKeyHeader: ReturnType<typeof Define> |
| 164 | +// ---cut--- |
| 165 | +// schemas/index.ts kelompokkan kontrak per sumber |
| 166 | +export const createTransferSchema = { |
| 167 | + json: Transfer |
| 168 | +} |
| 169 | + |
| 170 | +export const listAccountsSchema = { |
| 171 | + query: AccountQuery, |
| 172 | + headers: ApiKeyHeader |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +Sebuah rute mengimpor hanya tipe schema yang dibutuhkannya, sehingga handler tetap fokus pada respons alih-alih aturan: |
| 177 | + |
| 178 | +```typescript twoslash |
| 179 | +import { type Context, Define, Validator } from '@neabyte/deserve' |
| 180 | +const createTransferSchema = { json: Define((body: { amount: number }) => ({ amount: body.amount })) } |
| 181 | +// ---cut--- |
| 182 | +// routes/transfers.ts baca body tervalidasi |
| 183 | +export function POST(ctx: Context): Response { |
| 184 | + const { json } = Validator.read<typeof createTransferSchema>(ctx) |
| 185 | + return ctx.send.json({ amount: json.amount }, { status: 201 }) |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +Ini sebuah saran, bukan aturan. Aplikasi kecil menyimpan kontrak inline di samping rute, dan yang lebih besar memisahkannya begitu sebuah kontrak layak dipakai ulang. |
| 190 | + |
| 191 | +## Langkah Berikutnya |
| 192 | + |
| 193 | +- [Middleware Validator](/id/middleware/validation/validator-middleware) - pendaftaran per sumber yang dibungkus pola ini. |
| 194 | +- [Membaca Data Tervalidasi](/id/middleware/validation/reading-data) - memvalidasi params di handler di samping pola ini. |
| 195 | +- [Middleware Spesifik Rute](/id/middleware/route-specific) - aturan pencocokan prefix di baliknya. |
| 196 | +- [Ringkasan Validasi](/id/middleware/validation/overview) - bagaimana semua bagian saling terhubung. |
0 commit comments