0
# Form Actions
1
2
SvelteKit provides utilities for handling form submissions with progressive enhancement, validation, and proper error handling.
3
4
## Capabilities
5
6
### Fail Function
7
8
Creates an ActionFailure object for form submission failures, typically used for validation errors or processing failures.
9
10
```typescript { .api }
11
/**
12
* Create an ActionFailure object. Call when form submission fails.
13
* @param status - HTTP status code (must be 400-599)
14
* @param data - Data associated with the failure (e.g. validation errors)
15
* @returns ActionFailure object
16
*/
17
function fail(status: number): ActionFailure<undefined>;
18
function fail<T>(status: number, data: T): ActionFailure<T>;
19
```
20
21
**Usage Examples:**
22
23
```typescript
24
import { fail } from '@sveltejs/kit';
25
26
// Basic validation failure
27
export const actions = {
28
default: async ({ request }) => {
29
const data = await request.formData();
30
const email = data.get('email');
31
32
if (!email) {
33
return fail(400, { message: 'Email is required' });
34
}
35
36
if (!isValidEmail(email)) {
37
return fail(400, { message: 'Invalid email format' });
38
}
39
40
// Process successful form...
41
return { success: true };
42
}
43
};
44
45
// Multiple validation errors
46
export const actions = {
47
register: async ({ request }) => {
48
const data = await request.formData();
49
const errors = {};
50
51
const email = data.get('email');
52
const password = data.get('password');
53
const confirmPassword = data.get('confirmPassword');
54
55
if (!email) errors.email = 'Email is required';
56
if (!password) errors.password = 'Password is required';
57
if (password !== confirmPassword) {
58
errors.confirmPassword = 'Passwords do not match';
59
}
60
61
if (Object.keys(errors).length > 0) {
62
return fail(400, { errors });
63
}
64
65
try {
66
await createUser({ email, password });
67
return { success: true };
68
} catch (error) {
69
return fail(500, { message: 'Registration failed' });
70
}
71
}
72
};
73
```
74
75
### Action Failure Type Guard
76
77
Checks whether an object is an ActionFailure returned by the `fail()` function.
78
79
```typescript { .api }
80
/**
81
* Checks whether this is an action failure thrown by fail().
82
* @param e - The object to check
83
* @returns Type predicate indicating if e is an ActionFailure
84
*/
85
function isActionFailure(e: unknown): boolean;
86
```
87
88
**Usage Examples:**
89
90
```typescript
91
import { isActionFailure, fail } from '@sveltejs/kit';
92
93
export const actions = {
94
process: async ({ request }) => {
95
try {
96
const result = await someAsyncOperation();
97
return result;
98
} catch (error) {
99
if (isActionFailure(error)) {
100
// Re-throw ActionFailure
101
throw error;
102
}
103
104
// Handle other errors
105
return fail(500, { message: 'Processing failed' });
106
}
107
}
108
};
109
```
110
111
## Action Patterns
112
113
### Basic Form Action
114
115
```typescript
116
// src/routes/contact/+page.server.js
117
import { fail } from '@sveltejs/kit';
118
119
export const actions = {
120
default: async ({ request }) => {
121
const data = await request.formData();
122
const name = data.get('name');
123
const email = data.get('email');
124
const message = data.get('message');
125
126
// Validation
127
if (!name || !email || !message) {
128
return fail(400, {
129
error: 'All fields are required',
130
name,
131
email,
132
message
133
});
134
}
135
136
// Process form
137
try {
138
await sendContactEmail({ name, email, message });
139
return { success: true };
140
} catch (error) {
141
return fail(500, { error: 'Failed to send message' });
142
}
143
}
144
};
145
```
146
147
### Multiple Named Actions
148
149
```typescript
150
// src/routes/admin/users/[id]/+page.server.js
151
import { fail, redirect, error } from '@sveltejs/kit';
152
153
export const actions = {
154
update: async ({ request, params }) => {
155
const data = await request.formData();
156
const user = await getUser(params.id);
157
158
if (!user) {
159
throw error(404, 'User not found');
160
}
161
162
try {
163
await updateUser(params.id, {
164
name: data.get('name'),
165
email: data.get('email')
166
});
167
168
return { success: true, message: 'User updated' };
169
} catch (err) {
170
return fail(400, { error: 'Update failed' });
171
}
172
},
173
174
delete: async ({ params }) => {
175
const user = await getUser(params.id);
176
177
if (!user) {
178
throw error(404, 'User not found');
179
}
180
181
try {
182
await deleteUser(params.id);
183
throw redirect(303, '/admin/users');
184
} catch (err) {
185
return fail(500, { error: 'Delete failed' });
186
}
187
}
188
};
189
```
190
191
### File Upload Action
192
193
```typescript
194
import { fail } from '@sveltejs/kit';
195
import { writeFile } from 'fs/promises';
196
import { join } from 'path';
197
198
export const actions = {
199
upload: async ({ request }) => {
200
const data = await request.formData();
201
const file = data.get('file');
202
203
if (!file || !(file instanceof File)) {
204
return fail(400, { error: 'No file uploaded' });
205
}
206
207
if (file.size > 1024 * 1024) { // 1MB limit
208
return fail(400, { error: 'File too large' });
209
}
210
211
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
212
if (!allowedTypes.includes(file.type)) {
213
return fail(400, { error: 'Invalid file type' });
214
}
215
216
try {
217
const buffer = Buffer.from(await file.arrayBuffer());
218
const filename = `${Date.now()}-${file.name}`;
219
const filepath = join('uploads', filename);
220
221
await writeFile(filepath, buffer);
222
223
return { success: true, filename };
224
} catch (error) {
225
return fail(500, { error: 'Upload failed' });
226
}
227
}
228
};
229
```
230
231
## Client-Side Integration
232
233
### Basic Form Handling
234
235
```svelte
236
<!-- src/routes/contact/+page.svelte -->
237
<script>
238
import { enhance } from '$app/forms';
239
240
export let form;
241
</script>
242
243
<form method="POST" use:enhance>
244
<input
245
name="name"
246
placeholder="Name"
247
value={form?.name ?? ''}
248
required
249
/>
250
251
<input
252
name="email"
253
type="email"
254
placeholder="Email"
255
value={form?.email ?? ''}
256
required
257
/>
258
259
<textarea
260
name="message"
261
placeholder="Message"
262
value={form?.message ?? ''}
263
required
264
></textarea>
265
266
<button type="submit">Send Message</button>
267
268
{#if form?.error}
269
<p class="error">{form.error}</p>
270
{/if}
271
272
{#if form?.success}
273
<p class="success">Message sent successfully!</p>
274
{/if}
275
</form>
276
```
277
278
### Enhanced Form with Loading State
279
280
```svelte
281
<script>
282
import { enhance } from '$app/forms';
283
284
export let form;
285
286
let loading = false;
287
</script>
288
289
<form
290
method="POST"
291
use:enhance={() => {
292
loading = true;
293
294
return async ({ result, update }) => {
295
loading = false;
296
297
if (result.type === 'success') {
298
// Optional: reset form or show success message
299
}
300
301
await update();
302
};
303
}}
304
>
305
<!-- form fields -->
306
307
<button type="submit" disabled={loading}>
308
{loading ? 'Sending...' : 'Send Message'}
309
</button>
310
</form>
311
```
312
313
### Multiple Actions
314
315
```svelte
316
<script>
317
export let data;
318
export let form;
319
</script>
320
321
<!-- Update user -->
322
<form method="POST" action="?/update">
323
<input name="name" value={data.user.name} />
324
<input name="email" value={data.user.email} />
325
<button type="submit">Update</button>
326
</form>
327
328
<!-- Delete user -->
329
<form method="POST" action="?/delete">
330
<button type="submit" onclick="return confirm('Are you sure?')">
331
Delete User
332
</button>
333
</form>
334
335
{#if form?.error}
336
<p class="error">{form.error}</p>
337
{/if}
338
```
339
340
## Types
341
342
### ActionFailure Interface
343
344
```typescript { .api }
345
interface ActionFailure<T = undefined> {
346
/** HTTP status code (400-599) */
347
status: number;
348
/** Data associated with the failure */
349
data: T;
350
}
351
```
352
353
### Action Function Type
354
355
```typescript { .api }
356
type Action<
357
Params = Record<string, string>,
358
OutputData = Record<string, any> | void
359
> = (event: RequestEvent<Params>) => Promise<OutputData> | OutputData;
360
361
type Actions<
362
Params = Record<string, string>,
363
OutputData = Record<string, any> | void
364
> = Record<string, Action<Params, OutputData>>;
365
```
366
367
### Action Result Types
368
369
```typescript { .api }
370
type ActionResult<
371
Success = Record<string, unknown> | undefined,
372
Failure = Record<string, unknown> | undefined
373
> =
374
| { type: 'success'; status: number; data?: Success }
375
| { type: 'failure'; status: number; data?: Failure }
376
| { type: 'redirect'; status: number; location: string }
377
| { type: 'error'; status?: number; error: any };
378
```
379
380
## Best Practices
381
382
1. **Always validate input**: Check form data before processing
383
2. **Preserve form data on failure**: Return submitted values so users don't lose their input
384
3. **Use appropriate status codes**: 400 for validation errors, 500 for server errors
385
4. **Provide helpful error messages**: Users should understand what went wrong
386
5. **Handle file uploads carefully**: Validate file types, sizes, and sanitize filenames
387
6. **Use progressive enhancement**: Forms should work without JavaScript
388
7. **Redirect after successful mutations**: Use 303 redirects after successful POST/PUT/DELETE
389
8. **Consider security**: Validate CSRF tokens, sanitize input, check permissions