---
layout: 'page'
uri: '/guides/forms'
position: 4
slug: 'guides-forms'
parent: 'guides'
navTitle: 'Forms & Validation'
title: 'Forms & Validation'
description: 'Jak napsat formulář, který mluví s API a renderuje chyby z backendu. Validace žije v doméně, frontend ji jen propisuje.'
---
# Forms & Validation
Validace je **server-side**. Frontend data nevaliduje, jen pošle a zobrazí, co backend vrátí. Chyby chodí jako `{ field: message }` — klíč přesně odpovídá poli ve formuláři.
## Princip v pěti krocích
1. Formulář pošle JSON přes `authFetch`.
2. Command/query handler vytvoří Value Objecty. První chybná VO vrací `*shared.ValidationError{Field, Message}`.
3. `response.Error()` zapíše JSON `{ "<field>": "<message>" }` se správným HTTP statusem (400 / 401 / 403).
4. Frontend: `errors.value = result.data`. Klíče z backendu **1:1** sedí na typ errorů.
5. `<Input :error="errors.field" />` a `<ErrorAlert :message="errors.general" />` renderují.
## Backend: validace v doméně
Value Object je místo, kde pravidla žijí. Ne v handleru, ne ve formuláři.
```go
// domain/user/nickname.go
func NewNickname(s string) (Nickname, error) {
if s == "" {
return "", &shared.ValidationError{Field: "nickname", Message: "nickname is required"}
}
if len(s) > 50 {
return "", &shared.ValidationError{Field: "nickname", Message: "nickname must be at most 50 characters"}
}
return Nickname(s), nil
}
```
Handler volá VO za sebou a vrací první chybu, kterou potká:
```go
func (h *CreateUserHandler) Handle(ctx context.Context, cmd CreateUserCommand) error {
nickname, err := user.NewNickname(cmd.Nickname)
if err != nil { return err } // → 400 { "nickname": "…" }
role, err := user.NewRole(cmd.Role)
if err != nil { return err } // → 400 { "role": "…" }
if existing != nil {
return &shared.ValidationError{
Field: "nickname",
Message: "user with this nickname already exists",
}
}
// ... hash, save, collect event
}
```
Detaily error typů viz [Errors & Events](/framework/domain/errors-events), mapování na HTTP viz [HTTP Server](/framework/presentation/http-server).
## Response shape
Jedna chyba, jeden klíč:
```json
{ "nickname": "nickname is required" } // ValidationError s polem
{ "general": "invalid credentials" } // AuthError / PermissionError / bez pole
```
Víc klíčů najednou nechodí — handler vrací první chybu, na kterou narazí.
## Frontend: pass-through
### 1. Typ chyb
Každý formulář si napíše, jaké klíče očekává. Všechny optional.
```typescript
type ChangePasswordErrors = {
general?: string;
old_password?: string;
new_password?: string;
};
```
### 2. State
```typescript
const errors = ref<ChangePasswordErrors>({});
```
Prázdný objekt = bez chyb. Klíč existuje = pole má chybu. `delete` klíče = chyba zmizela.
### 3. Submit
```typescript
const handleSubmit = async (): Promise<void> => {
errors.value = {};
isLoading.value = true;
const result = await authFetch<null, ChangePasswordErrors>(
'PUT',
'/api/v1/profile/password',
{ body: form },
);
isLoading.value = false;
if (result.success === false) {
errors.value = result.data; // ⬅ backend klíče = klíče typu
return;
}
success('Password changed.');
resetForm();
};
```
Klíčová řádka: `errors.value = result.data`. Žádné mapování, žádné `if` nad kódem chyby. Backend určí klíč, typ ho vynutí, render ho zobrazí.
### 4. Render
```html
<Input
v-model="form.old_password"
:error="errors.old_password"
name="old_password"
type="password"
label="Current password"
required
:disabled="isLoading"
@update:model-value="() => clearFieldError('old_password')"
/>
<ErrorAlert :message="errors.general" />
```
Per-field chyba → `Input.error`. Obecná (login failed, rate limit, …) → `ErrorAlert`.
### 5. Čištění při editaci
```typescript
const clearFieldError = (field: keyof ChangePasswordErrors): void => {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- optional key removal is the intended API
delete errors.value[field];
};
```
Uživatel opraví pole → chyba zmizí. `authFetch` na 401 automaticky refreshne token — o auth stav se formulář nestará.
## Proč ne validovat na frontendu
- **Single source of truth** je doména. Duplikát pravidel se dřív nebo později rozejde.
- **Bezpečnost.** Frontend validace je jen UX — útočník ji obejde. Backend validuje tak jako tak.
- **Konzistence hlášek.** Stejný handler volá HTTP, CLI i test. Všude stejná zpráva pro stejný případ.
**Nativní asistence prohlížeče** funguje jen přes atributy, které `<Input>` reálně propíše na DOM — dnes pouze `type` (takže `type="email"` dá nativní kontrolu formátu). Pozor: prop `required` na `<Input>` je **čistě vizuální** — vykreslí hvězdičku `*` u labelu, ale nepropisuje HTML atribut `required`, takže z něj žádný browser tooltip nepřijde; `minlength`/`pattern` `<Input>` zatím nepropisuje vůbec. Pravda tak jako tak přichází z backendu.
## Kam dál
| Téma | Odkaz |
|---|---|
| `ValidationError`, `AuthError`, `PermissionError` | [Errors & Events](/framework/domain/errors-events) |
| JSON response + `FieldError` interface | [HTTP Server](/framework/presentation/http-server) |
| `authFetch` + auto-refresh na 401 | [Frontend Utils](/guides/frontend-utils) |
| Login / refresh / logout tok | [Authentication](/guides/auth) |
| Permission check v handleru | [Permissions](/guides/permissions) |
---
[← Frontend Utils](/guides/frontend-utils.md) | [Sentry →](/guides/sentry.md)