or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

app-server.mdapp-state-navigation.mdapp-state.mdconfiguration.mderror-handling.mdform-actions.mdhooks.mdindex.mdload-functions.mdnodejs-integration.mdrequest-handling.mdresponse-creation.mdservice-worker.mdvite-integration.md

hooks.mddocs/

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