Building API Endpoints
Every route in SvelteKit that has a +page.svelte file is a page route — it renders HTML for humans. But some URLs shouldn’t return HTML. They should return JSON for a mobile app, binary data for a file download, or an empty 204 response for a webhook acknowledgement. This is what +server.js (or +server.ts) is for.
A +server.js file in a route directory makes that URL respond to HTTP methods directly, without rendering any HTML. The same filesystem-based routing applies: src/routes/api/posts/+server.ts becomes the handler for GET /api/posts, POST /api/posts, and so on. You export a named function for each HTTP method you want to handle.
This means SvelteKit is a complete full-stack framework. Your backend API lives in src/routes/api/, your pages live in src/routes/, and they share code from src/lib/. One codebase, one deployment, no CORS configuration between your frontend and backend.
Co-locating API logicThe ability to co-locate API endpoints with your frontend is a significant productivity gain — no separate backend repository, no CORS headers to configure, shared TypeScript types between client and server. The constraint:
+server.jsfiles should contain HTTP handler logic, not business logic. Keep domain logic in$lib/server/where it can be shared and tested independently.
When to Use +server.js
Use +server.js when you need to:
- Return JSON data — For client-side fetch requests
- Build a REST API — CRUD operations for your data
- Handle webhooks — Receive data from external services
- Generate files — PDFs, images, CSV exports
- Proxy requests — Forward requests to external APIs
HTTP Method Handlers
In a +server.js file, you export functions named after HTTP methods:
// src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
// Handle GET requests
export const GET: RequestHandler = async () => {
const posts = await db.posts.findMany()
return json(posts)
}
// Handle POST requests
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json()
const post = await db.posts.create({ data })
return json(post, { status: 201 })
}
// Handle PUT requests
export const PUT: RequestHandler = async ({ request }) => {
const data = await request.json()
const post = await db.posts.update({
where: { id: data.id },
data
})
return json(post)
}
// Handle DELETE requests
export const DELETE: RequestHandler = async ({ request }) => {
const { id } = await request.json()
await db.posts.delete({ where: { id } })
return new Response(null, { status: 204 })
} The json Helper
API endpoints must return Response objects, not plain values. The json helper is a convenience wrapper that serializes your data with JSON.stringify, sets the Content-Type: application/json header automatically, and wraps the result in a proper Response. You could construct this manually, but json() makes it one line:
import { json } from '@sveltejs/kit'
// Equivalent to: new Response(JSON.stringify({ message: 'Hello' }), {
// headers: { 'Content-Type': 'application/json' }
// })
return json({ message: 'Hello' })
// With a custom status code
return json({ error: 'Not found' }, { status: 404 })
// With additional headers
return json(data, {
status: 200,
headers: {
'Cache-Control': 'max-age=3600'
}
}) When you need to return something that isn’t JSON — a file, plain text, a stream — you construct the Response directly, as shown in the “Returning Other Response Types” section below.
Request Handler Parameters
Each handler receives useful parameters:
export const POST: RequestHandler = async ({
request, // The Request object
params, // Route parameters
url, // URL object with searchParams
cookies, // Read/write cookies
locals, // Data from hooks
fetch, // Server-side fetch
platform, // Platform-specific context
setHeaders // Set response headers
}) => {
// Your handler logic
} Reading Request Data
JSON Body
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json()
// data is the parsed JSON body
return json({ received: data })
} Form Data
export const POST: RequestHandler = async ({ request }) => {
const formData = await request.formData()
const name = formData.get('name')
const email = formData.get('email')
return json({ name, email })
} URL Search Parameters
export const GET: RequestHandler = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1
const limit = Number(url.searchParams.get('limit')) || 10
const posts = await db.posts.findMany({
skip: (page - 1) * limit,
take: limit
})
return json({ posts, page, limit })
} Dynamic API Routes
Just like pages, API routes can have dynamic parameters:
// src/routes/api/posts/[id]/+server.ts
import { json, error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ params }) => {
const post = await db.posts.findUnique({
where: { id: params.id }
})
if (!post) {
error(404, 'Post not found')
}
return json(post)
}
export const DELETE: RequestHandler = async ({ params }) => {
await db.posts.delete({
where: { id: params.id }
})
return new Response(null, { status: 204 })
} Error Handling
Use the error helper to return error responses:
import { json, error } from '@sveltejs/kit'
export const GET: RequestHandler = async ({ params }) => {
const post = await db.posts.findUnique({
where: { id: params.id }
})
if (!post) {
error(404, 'Post not found')
}
return json(post)
} For more control, return a Response directly:
export const POST: RequestHandler = async ({ request }) => {
try {
const data = await request.json()
// ... process data
return json({ success: true })
} catch (e) {
return json({ error: 'Invalid JSON' }, { status: 400 })
}
} Authentication in API Routes
Access the user from locals (set by your hooks):
// src/routes/api/user/profile/+server.ts
import { json, error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user) {
error(401, 'Unauthorized')
}
return json({
id: locals.user.id,
name: locals.user.name,
email: locals.user.email
})
}
export const PUT: RequestHandler = async ({ locals, request }) => {
if (!locals.user) {
error(401, 'Unauthorized')
}
const data = await request.json()
const updated = await db.users.update({
where: { id: locals.user.id },
data: {
name: data.name,
bio: data.bio
}
})
return json(updated)
} Returning Other Response Types
Plain Text
export const GET: RequestHandler = async () => {
return new Response('Hello, World!', {
headers: {
'Content-Type': 'text/plain'
}
})
} HTML
export const GET: RequestHandler = async () => {
const html = `
<!DOCTYPE html>
<html>
<body><h1>Hello</h1></body>
</html>
`
return new Response(html, {
headers: {
'Content-Type': 'text/html'
}
})
} File Downloads
export const GET: RequestHandler = async () => {
const csv = 'name,email\nJohn,john@example.com'
return new Response(csv, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': 'attachment; filename="users.csv"'
}
})
} Streaming Responses
export const GET: RequestHandler = async () => {
const stream = new ReadableStream({
start(controller) {
controller.enqueue('Hello ')
setTimeout(() => {
controller.enqueue('World!')
controller.close()
}, 1000)
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/plain'
}
})
} Consuming API Routes
From Load Functions
// src/routes/blog/+page.ts
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('/api/posts')
return {
posts: await response.json()
}
} From Components
<script lang="ts">
let posts = $state([])
async function loadPosts() {
const response = await fetch('/api/posts')
posts = await response.json()
}
async function createPost(title: string) {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title })
})
if (response.ok) {
const newPost = await response.json()
posts = [...posts, newPost]
}
}
</script> CORS Configuration
For APIs consumed by external domains:
export const GET: RequestHandler = async () => {
return json(data, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS'
}
})
}
// Handle preflight requests
export const OPTIONS: RequestHandler = async () => {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
})
} When to Use +server.js vs Form Actions
Both +server.js and form actions (in +page.server.js) handle mutations on the server. The distinction comes down to who is calling them and what interface you want.
Form actions are the right choice when the mutation comes from an HTML form in your SvelteKit app. They work with JavaScript disabled, they support progressive enhancement with use:enhance, and they automatically invalidate and re-render the current page. They are a first-class SvelteKit pattern for HTML-form-based interactions.
+server.js is the right choice when you need a general-purpose HTTP endpoint: mobile apps calling your API, webhooks from Stripe or GitHub, programmatic fetch calls from components, or any consumer that isn’t an HTML form submission.
Use +server.js when… | Use Form Actions when… |
|---|---|
| External clients need to call the URL | The caller is always your SvelteKit app |
| Returning non-HTML responses (JSON…) | Handling an HTML form submission |
| Building a REST or RPC API | You want progressive enhancement |
| Handling webhooks from external services | You want automatic page revalidation |
Common Patterns
Newsletter Subscription
// src/routes/api/newsletter/+server.ts
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const POST: RequestHandler = async ({ request }) => {
const { email } = await request.json()
// Validate email
if (!email || !email.includes('@')) {
return json({ error: 'Invalid email' }, { status: 400 })
}
// Check if already subscribed
const existing = await db.subscribers.findUnique({
where: { email }
})
if (existing) {
return json({ error: 'Already subscribed' }, { status: 409 })
}
// Subscribe
await db.subscribers.create({
data: { email }
})
return json({ success: true }, { status: 201 })
} Health Check Endpoint
// src/routes/api/health/+server.ts
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async () => {
const dbHealthy = await checkDatabase()
return json(
{
status: dbHealthy ? 'healthy' : 'degraded',
timestamp: new Date().toISOString(),
version: '1.0.0'
},
{
status: dbHealthy ? 200 : 503
}
)
} Conclusion
The +server.js file transforms SvelteKit routes from purely page-rendering mechanisms into full API endpoints, enabling you to build RESTful APIs, webhooks, and data services directly within your application’s route structure. By exporting HTTP method handlers instead of page components, these files create endpoints that return JSON, files, or any response type you need.
This co-location of API logic with frontend code eliminates the traditional frontend/backend split for many applications, making full-stack development feel seamless and natural.
Mastering +server.js means understanding the full Web Fetch API surface—constructing Response objects, setting headers, handling streams, and implementing proper HTTP semantics. Combined with access to cookies for authentication, params for dynamic routing, and the ability to throw errors that cascade through SvelteKit’s error handling system, server endpoints provide everything needed to build production-grade APIs.
Whether building internal data fetching endpoints, public REST APIs, or webhook receivers, the +server.js pattern makes backend logic a natural extension of your routing system.
Key Takeaways
+server.jscreates API endpoints by exporting HTTP method handlers (GET,POST,PUT,DELETE) instead of rendering pages- Return Response objects using helpers like
json()for JSON responses,text()for plain text, or new Response() for custom types - Access route parameters via
paramsobject, URL search params viaurl.searchParams, and cookies for authentication - Use
error()for structured errors that flow through SvelteKit’s error handling with proper HTTP status codes - Co-locate API logic with frontend - place endpoints alongside pages in your route structure for related functionality
- Support any response type - JSON, text, files, streams, or custom MIME types via the Response constructor
- Perfect for webhooks and integrations - receive POST requests from external services, validate signatures, process data server-side
- Access server-only context via
localsfor request-scoped data,cookiesfor session management, andplatformfor deployment environment
See Also
- Official SvelteKit Documentation - +server.js
- RequestHandler Type - Function signature for handlers
json()helper - JSON response utility- Web Fetch API - Standard Response and Request objects
- API Route Patterns - REST API design with SvelteKit
- Streaming Responses - Server-sent events and streaming data