# Validasi formulir HTML di browser dan di server

> Validasi formulir dengan required, type, pattern, dan min, beri gaya error dengan :user-invalid, dan terapkan aturan yang sama di server dengan definisi field.

Terbit 2026-10-25 · https://ikiform.com/id/guides/html-form-submission/validation

Gunakan batasan bawaan HTML, seperti `required`, `type="email"`, `min`, `max`, dan `pattern`, agar browser memblokir input yang salah sebelum apa pun dikirim. Lalu terapkan aturan yang sama di server, karena bot dan script sepenuhnya melewati browser. Dengan Ikiform, definisikan field formulir dan setiap post diperiksa: yang salah mendapat `422 validation_failed` dengan pesan per field, yang bisa Anda tampilkan dengan `setCustomValidity`.

Contohnya adalah formulir pemesanan workshop. Formulir ini punya jenis field yang membuat validasi benar-benar berguna: jumlah tim dengan batas, tanggal yang tidak boleh di masa lalu, nomor telepon dalam satu format, dan kode booking dengan bentuk tetap.

## Apa yang berbeda di sini

Ada dua validator, dan keduanya tidak melihat request yang sama. Browser hanya memeriksa orang yang memakai halaman Anda, dengan atau tanpa script. Server memeriksa semuanya, termasuk bot yang mengirim langsung ke endpoint dan kode Anda sendiri yang memanggil `form.submit()`, yang melewati pemeriksaan browser. Pekerjaannya adalah membuat keduanya mengatakan hal yang sama.

Diagram: Post yang melewati validasi browser lalu validasi field Ikiform

1. Seseorang mengirim formulir, dan browser memeriksa batasan di HTML. Field yang tidak valid mendapat pesan dan tidak ada yang dikirim.
2. Post yang valid sampai ke Ikiform, yang memeriksanya terhadap definisi field formulir dan menyimpan nilai bertipe.
3. Bot atau script mengirim langsung ke endpoint, jadi tidak ada pemeriksaan browser yang berjalan.
4. Ikiform memeriksanya terhadap definisi yang sama dan menolaknya dengan 422 sebelum apa pun disimpan.

Lapisan browser untuk manusia. Lapisan server yang benar-benar menahan.

## Lapisan browser

Formulir: Book a workshop

- **Nama Anda** (`name`, text, wajib)
- **Email kantor** (`email`, email, wajib)
- **Jumlah peserta** (`team_size`, number, wajib)
- **Tanggal pilihan** (`date`, date, wajib)
- **Telepon** (`phone`, phone)
- **Kode booking** (`code`, text)

Definisi yang ditegakkan Ikiform. Coba jumlah tim 25, tanggal sebelum 2 November, atau kode seperti `ws-12`.

`workshop.html`:

```html
<form id="workshop" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="name">Your name</label>
  <input id="name" name="name" autocomplete="name" maxlength="120" required />

  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="team_size">People attending</label>
  <input id="team_size" name="team_size" type="number" min="2" max="20" step="1" required />

  <label for="date">Preferred date</label>
  <input id="date" name="date" type="date" min="2026-11-02" required />

  <label for="phone">Phone</label>
  <input id="phone" name="phone" type="tel" autocomplete="tel" placeholder="+14155550123"
    pattern="\+[1-9][0-9]{6,14}" title="International format, like +14155550123" />

  <label for="code">Booking code</label>
  <input id="code" name="code" pattern="WS-[0-9]{4}" title="Codes look like WS-1234" />

  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Request the workshop</button>
</form>
```

Setiap atribut dipetakan ke satu validity state, yang bisa Anda baca dari `input.validity` di console:

| Atribut | Validity state | Catatan |
| --- | --- | --- |
| `required` | `valueMissing` | Untuk grup radio, satu radio dengan `required` mencakup seluruh grup |
| `type="email"`, `type="url"` | `typeMismatch` | Longgar: `a@b` lolos sebagai email di browser |
| `pattern` | `patternMismatch` | Harus cocok dengan seluruh nilai. Diabaikan pada field opsional yang kosong |
| `minlength`, `maxlength` | `tooShort`, `tooLong` | Hanya diperiksa setelah orang mengedit nilainya |
| `min`, `max` | `rangeUnderflow`, `rangeOverflow` | Angka, tanggal, dan waktu |
| `step` | `stepMismatch` | `step="1"` menolak desimal |
| Input yang tidak bisa di-parse | `badInput` | Huruf yang diketik ke field angka di beberapa browser |
| `setCustomValidity()` | `customError` | Pesan apa pun yang Anda atur, sampai Anda mengosongkannya lagi |

### Pattern di dua engine

Browser mengompilasi `pattern` dengan flag `v`, yang lebih ketat daripada regular expression yang biasa ditulis orang: `-` literal di dalam character class harus di-escape, seperti `[a-z\-]`. Ikiform mengompilasi `pattern` field dengan flag `u` dan juga mencocokkan seluruh nilai. Tulis pattern yang valid di keduanya, seperti kode booking di atas, dan uji masing-masing di preview formulir sebelum Anda memublikasikannya.

### Memberi gaya pada error

`:invalid` cocok dengan field wajib yang kosong begitu halaman dimuat, jadi setiap field tampil merah sebelum ada yang mengetik. `:user-invalid` hanya cocok setelah orang berinteraksi dengan field atau mencoba mengirim, yang hampir selalu Anda inginkan:

`workshop.html`:

```html
<style>
  #workshop :user-invalid {
    border-color: #b42318;
  }
</style>
```

## Lapisan server

Tempel definisi di atas di tab **Fields** formulir. Sejak itu, Ikiform memvalidasi setiap post terhadapnya, dari sumber mana pun, dan menyimpan nilai yang sudah dikonversi: `team_size` sebagai angka, `phone` tanpa pemisah, `date` sebagai `YYYY-MM-DD`. Post yang melanggar aturan ditolak, tidak ada yang disimpan, dan jawabannya menyebut field mana yang gagal:

| Field | Dikirim | Pesan |
| --- | --- | --- |
| `team_size` | `25` | `Must be at most 20` |
| `team_size` | `2.5` | `Enter a whole number` |
| `date` | `2026-10-30` | `Must be on or after 2026-11-02` |
| `phone` | `415 555 0123` | `Use international format, e.g. +14155550123` |
| `code` | `ws-12` | `Codes look like WS-1234` |
| `website` | Nilai apa pun | `Unknown field "website"` |

Baris terakhir itu penting. Dengan field terdefinisi, Ikiform menolak field apa pun yang tidak ada di definisi, jadi input yang namanya diubah gagal secara jelas alih-alih disimpan dengan nama yang salah. Field yang diawali `_` dan honeypot tidak dihitung. Setiap tipe dan aturan ada di [Field dan validasi](https://ikiform.com/id/docs/fields.md).

### Dengan post biasa

Tidak perlu kode. Post biasa yang ditolak menampilkan halaman error hosted Ikiform dengan status `422`, setiap masalah di bawah label field Anda, dan tautan ke halaman asal post. Tombol **Back** browser biasanya mempertahankan isian orang itu; tautannya memuat ulang formulir dalam keadaan kosong.

### Menampilkan error server di field

Dengan script, masukkan setiap pesan server ke UI error bawaan browser. `setCustomValidity` menandai field tidak valid dengan teks itu, dan `reportValidity` menampilkannya sebagai gelembung biasa dan memfokuskan yang pertama:

`workshop.js`:

```js
const form = document.querySelector("#workshop");

for (const control of form.elements) {
  control.addEventListener("input", () => control.setCustomValidity?.(""));
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const response = await fetch(form.action, {
    method: "POST",
    headers: { Accept: "application/json" },
    body: new FormData(form),
  });
  const result = await response.json();
  if (result.ok) {
    form.replaceWith("Thanks. We'll confirm the date by email.");
    return;
  }
  const fields = result.error.fields ?? {};
  for (const [name, messages] of Object.entries(fields)) {
    form.elements.namedItem(name)?.setCustomValidity?.(messages[0]);
  }
  if (!form.reportValidity()) return;
  alert(result.error.message);
});
```

- Hapus pesan kustom saat `input`. Field dengan error kustom tetap tidak valid sampai Anda mengaturnya kembali ke string kosong, dan sampai saat itu orang sama sekali tidak bisa mengirim.
- Pemeriksaan browser tetap berjalan sebelum event `submit`, jadi server hanya melihat post yang sudah lolos.
- Jika error tidak punya `fields`, seperti `429 rate_limited`, `reportValidity()` mengembalikan `true` dan script kembali ke pesan umum.

## Memeriksa aturan server sebelum mengirim

Untuk menangkap aturan khusus server, seperti daftar domain email, sebelum request, muat definisi publik formulir dan jalankan `validate` dari [SDK](https://ikiform.com/id/docs/sdk.md), yang menerapkan aturan dan pesan field yang sama di browser:

`workshop.js`:

```js
import { getSchema, validate } from "@ikiform/sdk";

const form = document.querySelector("#workshop");
const result = await getSchema("frm_xxxxxxxxxxxxxxxx");
const fields = result.ok ? result.schema.fields : [];

form.addEventListener("submit", (event) => {
  const errors = validate(fields, new FormData(form));
  for (const control of form.elements) control.setCustomValidity?.(errors[control.name] ?? "");
  if (!form.reportValidity()) event.preventDefault();
});
```

> Anggap ini sebagai kemudahan. Server tetap menjadi sumber kebenaran, dan `getSchema` hanya mengekspos definisi field, yang memang publik: siapa pun bisa membacanya dari `/f/<formId>/schema`.

## Pastikan berfungsi

1. Di **Allowed domains** formulir, tambahkan domain situs Anda, ditambah `localhost` selama pengembangan. Biarkan kosong untuk menerima kiriman dari mana saja.
2. Kirim formulir kosong. Browser menghentikannya di **Your name** dan tidak ada yang muncul di panel Network.
3. Hapus `max` dari jumlah tim di developer tools, masukkan `25`, lalu kirim. Post biasa menampilkan halaman error Ikiform; versi script menampilkan **Must be at most 20** di field.
4. Kirim dengan curl, melewati browser: `curl -H "Accept: application/json" -H "Origin: https://example.com" -d team_size=abc https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx`, dengan allowed domain Anda sendiri sebagai origin. Jawabannya `422` dengan pesan untuk `team_size` dan setiap field wajib yang hilang.
5. Buka **Submissions** di dashboard. Kiriman baru ada di sana dengan setiap field yang Anda kirim.

## Hal yang sering menjebak

| Masalah | Penyebab dan solusi |
| --- | --- |
| Formulir tidak pernah terkirim setelah error server | Pesan custom validity tidak pernah dihapus. Reset dengan `setCustomValidity("")` saat `input` |
| `minlength` tidak terpicu pada nilai yang sudah terisi | `tooShort` hanya berlaku setelah orang mengedit field. Aturan server yang menangkapnya |
| `pattern` diabaikan sepenuhnya | Pattern itu tidak valid di bawah flag `v`, dan browser melewati pattern yang tidak valid sepenuhnya. Escape `-` dan karakter khusus lain di dalam class |
| Error tampil sebelum ada yang mengetik | CSS memakai `:invalid`. Ganti ke `:user-invalid` |
| `422 validation_failed` | Kiriman tidak cocok dengan definisi field formulir. `error.fields` menyebutkan setiap field dan alasannya. Samakan `name` input dengan ID field. |

Alur pengiriman lengkap ada di [ringkasan](https://ikiform.com/id/guides/html-form-submission.md#validation), dan [Errors](https://ikiform.com/id/docs/errors.md) mencantumkan setiap kode yang bisa didapat sebuah post.

## Pertanyaan

### Apakah validasi formulir HTML saja sudah cukup?

Tidak. Validasi itu hanya berjalan di browser di halaman Anda, dan bot atau script mengirim langsung ke endpoint. Validasi juga di server. Dengan Ikiform, definisikan field formulir dan setiap post diperiksa.

### Bagaimana cara menampilkan pesan error server di input?

Panggil input.setCustomValidity(message) lalu form.reportValidity(), yang menampilkan pesan di gelembung biasa browser. Hapus dengan setCustomValidity("") saat orang mengedit field.

### Apa bedanya :invalid dan :user-invalid?

:invalid cocok dengan field apa pun yang gagal memenuhi batasannya, termasuk field wajib yang kosong saat halaman dimuat. :user-invalid hanya cocok setelah orang berinteraksi dengan field atau mencoba mengirim.

### Apa yang dikembalikan Ikiform saat sebuah field gagal validasi?

422 validation_failed tanpa ada yang disimpan. Request JSON mendapat error.fields, yang memetakan setiap id field ke pesannya; form post biasa mendapat halaman error hosted yang mencantumkannya per label field.

## Panduan lainnya

- [Pengiriman formulir HTML](https://ikiform.com/id/guides/html-form-submission.md): Apa yang dilakukan browser saat formulir dikirim: field mana yang dikirim, bagaimana di-encode, apa yang seharusnya dijawab server, dan bagaimana Ikiform menjawab.
- [Tanpa reload halaman](https://ikiform.com/id/guides/html-form-submission/without-page-reload.md): Kirim formulir dengan fetch dan FormData agar pengunjung tetap di halaman, dengan error field, status loading, percobaan ulang, dan cadangan saat script gagal.
- [Atribut action](https://ikiform.com/id/guides/html-form-submission/action-attribute.md): Apa fungsi atribut action formulir, bagaimana nilai relatif dan kosong di-resolve, bagaimana formaction mengirim satu formulir ke dua tempat, dan mengapa host statis menjawab 405.
- [Redirect setelah submit](https://ikiform.com/id/guides/html-form-submission/redirect-after-submit.md): Arahkan pengunjung ke halaman terima kasih Anda sendiri setelah formulir terkirim: redirect 303, field _redirect, domain mana yang diizinkan, dan apa efek refresh.
