0
# Hooks
1
2
SvelteKit hooks provide a way to intercept and customize request/response handling at the application level. The `sequence` function allows you to compose multiple hooks together.
3
4
## Capabilities
5
6
### Hook Composition
7
8
Compose multiple handle functions with middleware-like behavior.
9
10
```typescript { .api }
11
/**
12
* A helper function for sequencing multiple handle calls in a middleware-like manner.
13
* The behavior for the handle options is as follows:
14
* - transformPageChunk is applied in reverse order and merged
15
* - preload is applied in forward order, the first option "wins"
16
* - filterSerializedResponseHeaders behaves the same as preload
17
* @param handlers - Array of handle functions to sequence
18
* @returns Combined handle function
19
*/
20
function sequence(...handlers: Handle[]): Handle;
21
```
22
23
### Handle Hook Type
24
25
The core hook type for request/response interception.
26
27
```typescript { .api }
28
/**
29
* The handle hook runs every time the SvelteKit server receives a request
30
* and determines the response. It receives an event object representing
31
* the request and a function called resolve, which renders the route
32
* and generates a Response.
33
*/
34
type Handle = (input: {
35
event: RequestEvent;
36
resolve: (event: RequestEvent, opts?: ResolveOptions) => Promise<Response>;
37
}) => Promise<Response>;
38
39
interface ResolveOptions {
40
/** Applies custom transforms to HTML chunks */
41
transformPageChunk?: (input: { html: string; done: boolean }) => Promise<string | undefined> | string | undefined;
42
/** Determines which headers should be included in serialized responses */
43
filterSerializedResponseHeaders?: (name: string, value: string) => boolean;
44
/** Determines what should be added to the <head> tag to preload resources */
45
preload?: (input: { type: 'font' | 'css' | 'js' | 'asset'; path: string }) => boolean;
46
}
47
```
48
49
## Hook Patterns
50
51
### Basic Authentication Hook
52
53
```typescript
54
// src/hooks.server.js
55
import { redirect } from '@sveltejs/kit';
56
57
export async function handle({ event, resolve }) {
58
// Parse session from cookie
59
const sessionId = event.cookies.get('session');
60
61
if (sessionId) {
62
const user = await getUserBySessionId(sessionId);
63
event.locals.user = user;
64
}
65
66
// Protect admin routes
67
if (event.url.pathname.startsWith('/admin')) {
68
if (!event.locals.user?.isAdmin) {
69
throw redirect(303, '/login');
70
}
71
}
72
73
// Protect authenticated routes
74
if (event.url.pathname.startsWith('/dashboard')) {
75
if (!event.locals.user) {
76
throw redirect(303, '/login');
77
}
78
}
79
80
return resolve(event);
81
}
82
```
83
84
### Request Logging Hook
85
86
```typescript
87
// src/hooks.server.js
88
export async function handle({ event, resolve }) {
89
const startTime = Date.now();
90
const requestId = crypto.randomUUID();
91
92
// Add request ID to locals
93
event.locals.requestId = requestId;
94
95
console.log(`[${requestId}] ${event.request.method} ${event.url.pathname}`);
96
97
const response = await resolve(event);
98
99
const duration = Date.now() - startTime;
100
console.log(`[${requestId}] ${response.status} ${duration}ms`);
101
102
// Add request ID to response headers
103
response.headers.set('X-Request-ID', requestId);
104
105
return response;
106
}
107
```
108
109
### CORS Hook
110
111
```typescript
112
// src/hooks.server.js
113
export async function handle({ event, resolve }) {
114
// Handle preflight requests
115
if (event.request.method === 'OPTIONS') {
116
return new Response(null, {
117
status: 200,
118
headers: {
119
'Access-Control-Allow-Origin': '*',
120
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
121
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
122
'Access-Control-Max-Age': '86400'
123
}
124
});
125
}
126
127
const response = await resolve(event);
128
129
// Add CORS headers to all responses
130
response.headers.set('Access-Control-Allow-Origin', '*');
131
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
132
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
133
134
return response;
135
}
136
```
137
138
### Content Security Policy Hook
139
140
```typescript
141
// src/hooks.server.js
142
export async function handle({ event, resolve }) {
143
const response = await resolve(event, {
144
transformPageChunk: ({ html }) => {
145
// Generate nonce for inline scripts
146
const nonce = crypto.randomUUID();
147
148
// Add nonce to inline scripts
149
html = html.replace(
150
/<script>/g,
151
`<script nonce="${nonce}">`
152
);
153
154
return html;
155
}
156
});
157
158
// Set CSP header
159
response.headers.set(
160
'Content-Security-Policy',
161
`default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline';`
162
);
163
164
return response;
165
}
166
```
167
168
### Multiple Hooks with Sequence
169
170
```typescript
171
// src/hooks.server.js
172
import { sequence } from '@sveltejs/kit/hooks';
173
174
// Individual hook functions
175
async function authHook({ event, resolve }) {
176
const sessionId = event.cookies.get('session');
177
if (sessionId) {
178
event.locals.user = await getUserBySessionId(sessionId);
179
}
180
return resolve(event);
181
}
182
183
async function loggingHook({ event, resolve }) {
184
const start = Date.now();
185
console.log(`→ ${event.request.method} ${event.url.pathname}`);
186
187
const response = await resolve(event);
188
189
const duration = Date.now() - start;
190
console.log(`← ${response.status} ${duration}ms`);
191
192
return response;
193
}
194
195
async function corsHook({ event, resolve }) {
196
if (event.request.method === 'OPTIONS') {
197
return new Response(null, {
198
status: 200,
199
headers: {
200
'Access-Control-Allow-Origin': '*',
201
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
202
'Access-Control-Allow-Headers': 'Content-Type'
203
}
204
});
205
}
206
207
const response = await resolve(event);
208
response.headers.set('Access-Control-Allow-Origin', '*');
209
210
return response;
211
}
212
213
async function securityHook({ event, resolve }) {
214
const response = await resolve(event);
215
216
// Add security headers
217
response.headers.set('X-Frame-Options', 'DENY');
218
response.headers.set('X-Content-Type-Options', 'nosniff');
219
response.headers.set('X-XSS-Protection', '1; mode=block');
220
221
return response;
222
}
223
224
// Combine all hooks
225
export const handle = sequence(
226
loggingHook, // Runs first
227
authHook, // Runs second
228
corsHook, // Runs third
229
securityHook // Runs last
230
);
231
```
232
233
### Advanced HTML Transformation
234
235
```typescript
236
// src/hooks.server.js
237
export async function handle({ event, resolve }) {
238
return resolve(event, {
239
transformPageChunk: ({ html, done }) => {
240
// Only transform the final chunk
241
if (!done) return html;
242
243
// Add analytics script
244
if (event.url.pathname !== '/admin') {
245
html = html.replace(
246
'</head>',
247
` <script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
248
<script>
249
window.dataLayer = window.dataLayer || [];
250
function gtag(){dataLayer.push(arguments);}
251
gtag('js', new Date());
252
gtag('config', 'GA_MEASUREMENT_ID');
253
</script>
254
</head>`
255
);
256
}
257
258
// Minify HTML in production
259
if (process.env.NODE_ENV === 'production') {
260
html = html.replace(/>\s+</g, '><').trim();
261
}
262
263
return html;
264
}
265
});
266
}
267
```
268
269
### Rate Limiting Hook
270
271
```typescript
272
// src/hooks.server.js
273
const rateLimitMap = new Map();
274
275
export async function handle({ event, resolve }) {
276
const clientIP = event.getClientAddress();
277
const now = Date.now();
278
const windowMs = 60 * 1000; // 1 minute
279
const maxRequests = 100;
280
281
// Get or create rate limit data for this IP
282
const ipData = rateLimitMap.get(clientIP) || { requests: [], blocked: false };
283
284
// Remove old requests outside the window
285
ipData.requests = ipData.requests.filter(time => time > now - windowMs);
286
287
// Check if rate limit exceeded
288
if (ipData.requests.length >= maxRequests) {
289
ipData.blocked = true;
290
rateLimitMap.set(clientIP, ipData);
291
292
return new Response('Too Many Requests', {
293
status: 429,
294
headers: {
295
'Retry-After': '60',
296
'X-RateLimit-Limit': maxRequests.toString(),
297
'X-RateLimit-Remaining': '0'
298
}
299
});
300
}
301
302
// Record this request
303
ipData.requests.push(now);
304
ipData.blocked = false;
305
rateLimitMap.set(clientIP, ipData);
306
307
const response = await resolve(event);
308
309
// Add rate limit headers
310
const remaining = Math.max(0, maxRequests - ipData.requests.length);
311
response.headers.set('X-RateLimit-Limit', maxRequests.toString());
312
response.headers.set('X-RateLimit-Remaining', remaining.toString());
313
314
return response;
315
}
316
```
317
318
### Database Connection Hook
319
320
```typescript
321
// src/hooks.server.js
322
import { connectToDatabase, closeDatabaseConnection } from '$lib/database';
323
324
let dbConnection = null;
325
326
export async function handle({ event, resolve }) {
327
// Ensure database connection
328
if (!dbConnection) {
329
dbConnection = await connectToDatabase();
330
}
331
332
// Add database to locals
333
event.locals.db = dbConnection;
334
335
try {
336
return await resolve(event);
337
} catch (error) {
338
// Log database errors
339
if (error.code?.startsWith('DB_')) {
340
console.error('Database error:', error);
341
}
342
throw error;
343
}
344
}
345
346
// Cleanup on process exit
347
process.on('SIGTERM', async () => {
348
if (dbConnection) {
349
await closeDatabaseConnection(dbConnection);
350
}
351
});
352
```
353
354
## Error Handling Hooks
355
356
### Server Error Hook
357
358
```typescript
359
// src/hooks.server.js
360
export async function handleError({ error, event, status, message }) {
361
const errorId = crypto.randomUUID();
362
363
// Log error with context
364
console.error(`[${errorId}] ${status} ${message}`, {
365
error: error.stack,
366
url: event.url.toString(),
367
userAgent: event.request.headers.get('user-agent'),
368
timestamp: new Date().toISOString()
369
});
370
371
// Send to error tracking service
372
if (process.env.NODE_ENV === 'production') {
373
await sendToErrorTracking({
374
errorId,
375
error,
376
context: {
377
url: event.url.toString(),
378
method: event.request.method,
379
userAgent: event.request.headers.get('user-agent')
380
}
381
});
382
}
383
384
// Return user-friendly error
385
return {
386
message: process.env.NODE_ENV === 'development' ? message : 'Internal error',
387
errorId
388
};
389
}
390
```
391
392
### Client Error Hook
393
394
```typescript
395
// src/hooks.client.js
396
export async function handleError({ error, event, status, message }) {
397
// Log client-side errors
398
console.error('Client error:', error);
399
400
// Send to analytics
401
if (typeof gtag !== 'undefined') {
402
gtag('event', 'exception', {
403
description: message,
404
fatal: status >= 500
405
});
406
}
407
408
return {
409
message: 'Something went wrong'
410
};
411
}
412
```
413
414
## Best Practices
415
416
1. **Order matters**: Use `sequence()` to control hook execution order
417
2. **Error handling**: Always handle errors gracefully in hooks
418
3. **Performance**: Keep hooks fast as they run on every request
419
4. **Security**: Use hooks for authentication, CORS, and security headers
420
5. **Logging**: Add request logging and error tracking
421
6. **Database connections**: Manage database connections in hooks
422
7. **Rate limiting**: Implement rate limiting for public APIs
423
8. **Clean up**: Properly clean up resources on process exit
424
9. **Environment awareness**: Adjust behavior based on development/production
425
10. **Transform carefully**: Use `transformPageChunk` sparingly for performance