Skip to content

Commit 71103cd

Browse files
committed
docs(validation): add validation middleware pages
- Add advanced patterns, define schema, and overview pages - Add reading validated data and validator middleware pages - Document the same pages in Indonesian
1 parent faa4e1e commit 71103cd

10 files changed

Lines changed: 1400 additions & 0 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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+
![Pola selectValidator: sebuah request pada prefix bersama mencapai pemilih yang membaca method dan pathname, mengembalikan schema membangun dan men-cache validator sekali sebelum handler, dan mengembalikan undefined memanggil next sehingga request mengalir lewat tanpa disentuh](/diagrams/validation-select-validator.png)
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+
![Urutan sumber lintas schema: kontrak query buruk melempar lebih dulu dan hanya melaporkan alasan query, sementara kontrak headers dan cookies yang datang setelahnya dalam urutan key tidak pernah berjalan](/diagrams/validation-source-order.png)
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.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
---
2+
description: "Bangun kontrak request dengan Define, sebuah transform yang dipasangkan dengan guard untuk menolak input buruk."
3+
---
4+
5+
# Define Schema
6+
7+
> **Referensi**: [Repositori GitHub Typebox](https://github.com/NeaByteLab/Typebox)
8+
9+
Sebuah kontrak adalah fungsi yang menerima satu input dan mengembalikan nilai yang sudah bersih. `Define` membangunnya dari dua bagian, sebuah transform yang membentuk output dan guard opsional yang menolak input sebelum transform berjalan.
10+
11+
## Bentuk Define
12+
13+
`Define(transform, guard?)` mengembalikan sebuah kontrak:
14+
15+
```typescript twoslash
16+
import { Define } from '@neabyte/deserve'
17+
18+
// Hanya transform, tanpa guard
19+
const Trim = Define((body: { name: string }) => ({
20+
name: body.name.trim()
21+
}))
22+
```
23+
24+
Transform menormalkan nilai, memangkas string, membuat email jadi huruf kecil, atau memaksa angka. Transform berjalan sebagai badan kontrak setelah input dipercaya.
25+
26+
Transform juga memiliki bentuk output. Guard yang lolos tidak membuang key tambahan, jadi field tak dikenal dari client tetap ada kecuali transform menghilangkannya. Mengembalikan object baru hanya dengan field yang diinginkan menjaga input kejutan keluar dari data tervalidasi:
27+
28+
```typescript twoslash
29+
import { Define } from '@neabyte/deserve'
30+
31+
// Output hanya menyimpan field yang disebut
32+
const NewUser = Define((body: { name: string; role: string }) => ({
33+
name: body.name.trim()
34+
}))
35+
```
36+
37+
Di sini client yang mengirim `role: 'admin'` mendapati nilainya dibuang, karena transform tidak pernah menyalinnya.
38+
39+
## Urutan Operasi
40+
41+
Memanggil sebuah kontrak menjalankan empat langkah dalam urutan tetap, dan transform hanya pernah melihat input yang sudah lolos setiap guard:
42+
43+
1. Input string yang lebih panjang dari 10000 karakter ditolak sebelum hal lain.
44+
2. Input object dibekukan dalam (deep frozen) agar guard tidak bisa memutasinya.
45+
3. Setiap guard berjalan berurutan, melempar pada kegagalan pertama.
46+
4. Transform berjalan dan mengembalikan nilai yang sudah bersih.
47+
48+
Kontrak tanpa guard langsung lompat ke transform, jadi transform harus memercayai inputnya atau melakukan pemeriksaan sendiri.
49+
50+
![Urutan operasi Define: sebuah kontrak pertama membatasi input string pada 10000 karakter, lalu membekukan dalam sebuah object agar guard tidak bisa memutasinya, lalu menjalankan tiap guard berurutan dengan melempar pada kegagalan pertama, dan baru kemudian menjalankan transform pada input yang sudah lolos setiap guard](/diagrams/validation-contract-order.png)
51+
52+
## Guard Menentukan Lolos Atau Gagal
53+
54+
Sebuah guard memeriksa input mentah dan mengembalikan keputusan:
55+
56+
- `true` saat input lolos.
57+
- Sebuah `string` untuk satu alasan kegagalan.
58+
- Sebuah `string[]` untuk beberapa alasan kegagalan sekaligus.
59+
60+
![Keputusan guard: mengembalikan true mengirim input ke transform, sementara mengembalikan sebuah string atau array string membuat kontrak melempar dan menjadi 422 dengan alasan itu terjaga di error.cause](/diagrams/validation-guard-verdict.png)
61+
62+
```typescript twoslash
63+
import { Define } from '@neabyte/deserve'
64+
65+
// Guard menolak nama kosong
66+
const NewUser = Define(
67+
(body: { name: string }) => ({ name: body.name.trim() }),
68+
(body) => (body.name.trim().length > 0 ? true : 'name must not be empty')
69+
)
70+
```
71+
72+
Guard yang mengembalikan alasan membuat kontrak melempar, dan validator mengubah lemparan itu menjadi 422 yang membawa alasan persis tersebut. Jalur dari sebuah alasan menuju respons ada di [Membaca Data Tervalidasi](/id/middleware/validation/reading-data#cara-kegagalan-muncul).
73+
74+
## Memeriksa Bentuk Lebih Dulu
75+
76+
Sebuah guard menerima input mentah, yang bisa `null`, sebuah array, atau nilai JSON apa pun yang dikirim client. Mengambil sebuah field pada bentuk yang salah akan melempar di dalam guard sebelum aturannya berjalan, jadi pemeriksaan bentuk datang lebih dulu:
77+
78+
```typescript twoslash
79+
import { Define } from '@neabyte/deserve'
80+
81+
// Pastikan object sebelum membaca field
82+
function isRecord(value: unknown): value is Record<string, unknown> {
83+
return value !== null && typeof value === 'object' && !Array.isArray(value)
84+
}
85+
86+
const NewUser = Define(
87+
(body: { name: string }) => ({ name: body.name.trim() }),
88+
(body) => {
89+
if (!isRecord(body)) {
90+
return 'body must be a JSON object'
91+
}
92+
return typeof body['name'] === 'string' ? true : 'name must be a string'
93+
}
94+
)
95+
```
96+
97+
Lemparan di dalam guard tetap menjadi 422, tidak pernah 500, jadi pemeriksaan bentuk yang terlewat gagal dengan aman alih-alih menjatuhkan request.
98+
99+
## Melaporkan Beberapa Field Sekaligus
100+
101+
Mengembalikan sebuah array melaporkan setiap field yang rusak dalam satu respons alih-alih satu per satu:
102+
103+
```typescript twoslash
104+
import { Define } from '@neabyte/deserve'
105+
106+
// Kumpulkan tiap kegagalan ke satu array
107+
const NewUser = Define(
108+
(body: { name: string; age: number }) => body,
109+
(body) => {
110+
const reasons: string[] = []
111+
if (body.name.trim().length === 0) {
112+
reasons.push('name must not be empty')
113+
}
114+
if (body.age < 18) {
115+
reasons.push('age must be at least 18')
116+
}
117+
return reasons.length === 0 ? true : reasons
118+
}
119+
)
120+
```
121+
122+
## Menyusun Beberapa Guard
123+
124+
Argumen kedua juga menerima array guard. Guard berjalan berurutan dan kontrak melempar pada yang pertama gagal, jadi guard berikutnya tidak pernah melihat input yang sudah ditolak guard sebelumnya:
125+
126+
```typescript twoslash
127+
import { Define } from '@neabyte/deserve'
128+
129+
// Cek bentuk dulu, aturan bisnis kedua
130+
function hasFields(body: { from: string; to: string }): true | string {
131+
return body.from && body.to ? true : 'from and to are required'
132+
}
133+
134+
function distinctAccounts(body: { from: string; to: string }): true | string {
135+
return body.from !== body.to ? true : 'from and to must differ'
136+
}
137+
138+
const Transfer = Define(
139+
(body: { from: string; to: string }) => body,
140+
[hasFields, distinctAccounts]
141+
)
142+
```
143+
144+
Memisahkan pemeriksaan bentuk dari aturan bisnis menjaga tiap guard tetap kecil dan membuat aturan lintas-field bisa berasumsi field-nya sudah ada.
145+
146+
## Pengaman Bawaan
147+
148+
Batas string dan pembekuan dari [Urutan Operasi](#urutan-operasi) berjalan otomatis, jadi sebuah kontrak tidak pernah membuang waktu pada payload raksasa dan sebuah guard tidak pernah memutasi nilai yang diperiksanya. Satu aturan lagi menjaga model waktunya:
149+
150+
- Guard async ditolak, karena validasi tetap sinkron dan dapat diprediksi.
151+
152+
Aturan ini berasal dari Typebox sendiri dan berlaku untuk setiap kontrak, baik yang berjalan lewat [Middleware Validator](/id/middleware/validation/validator-middleware) maupun panggilan `Validator.check` langsung.
153+
154+
## Langkah Berikutnya
155+
156+
- [Middleware Validator](/id/middleware/validation/validator-middleware) - menghubungkan kontrak ke sumber request.
157+
- [Membaca Data Tervalidasi](/id/middleware/validation/reading-data) - membaca output transform di handler.
158+
- [Pola Lanjutan](/id/middleware/validation/advanced-patterns) - menyusun guard dan mengatur urutan kegagalannya.
159+
- [Ringkasan Validasi](/id/middleware/validation/overview) - bagaimana semua bagian saling terhubung.

0 commit comments

Comments
 (0)