07 - API Routes
📋 Jump to TakeawaysWhat are API Routes?
Sometimes your app needs endpoints that return data instead of HTML pages. A login endpoint, a tracking endpoint, a way for your frontend to fetch or save data. In SvelteKit, you create these by adding a +server.ts file in any route folder. The file path becomes the URL.
Basic API Route
Let's say you're building a blog and need an endpoint to return posts. Create a file at src/routes/api/posts/+server.ts and it becomes available at /api/posts:
// src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
// In a real app, this would come from a database
const posts = [
{ id: 1, title: 'First Post' },
{ id: 2, title: 'Second Post' }
];
return json(posts); // json() sets Content-Type and serializes automatically
};Now GET http://localhost:5173/api/posts returns [{ id: 1, title: "First Post" }, ...]. The folder structure is the URL: src/routes/api/posts/ = /api/posts. The api/ prefix is just a convention, not a requirement.
HTTP Methods
A single +server.ts file can handle multiple HTTP methods. Export a function for each one you need. Here's a full CRUD example for posts:
// src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
// GET /api/posts?id=1 — read a post
export const GET: RequestHandler = async ({ url }) => {
const id = url.searchParams.get('id');
return json({ id });
};
// POST /api/posts — create a new post
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json();
return json({ created: data }, { status: 201 });
};
// PUT /api/posts — update an existing post
export const PUT: RequestHandler = async ({ request }) => {
const data = await request.json();
return json({ updated: data });
};
// DELETE /api/posts?id=1 — delete a post
export const DELETE: RequestHandler = async ({ url }) => {
const id = url.searchParams.get('id');
return json({ deleted: id });
};Each function name matches the HTTP method. If someone sends a PATCH request and you haven't exported PATCH, SvelteKit returns 405 Method Not Allowed automatically.
Dynamic API Routes
Use [param] folders just like page routes. The folder name becomes the parameter:
// src/routes/api/posts/[id]/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { posts } from '$lib/server/db/schema';
export const GET: RequestHandler = async ({ params }) => {
// Drizzle query — see the Drizzle ORM course for more
const post = await db.query.posts.findFirst({
where: eq(posts.id, params.id)
});
if (!post) {
error(404, 'Post not found'); // throws an error response
}
return json(post);
};Access at: GET http://localhost:5173/api/posts/42 — params.id will be "42".
Request Headers
Read incoming headers from the request:
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ request }) => {
const auth = request.headers.get('authorization');
if (!auth) {
error(401, 'Unauthorized');
}
return json({ authenticated: true });
};Response Headers
Set custom headers on the response:
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
return json(
{ data: 'value' },
{
headers: {
'Cache-Control': 'max-age=3600'
}
}
);
};Non-JSON Responses
Return plain text, HTML, or any other format using the standard Response object:
import type { RequestHandler } from './$types';
// Plain text
export const GET: RequestHandler = async () => {
return new Response('Plain text');
};// HTML
export const GET: RequestHandler = async () => {
return new Response('<h1>HTML</h1>', {
headers: { 'Content-Type': 'text/html' }
});
};Error Handling
Use try/catch and error() for proper HTTP error responses:
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ params }) => {
try {
const data = await fetchData(params.id);
return json(data);
} catch (e) {
error(500, 'Internal server error');
}
};CORS Headers
If another website or app needs to call your API, browsers will block it by default (CORS). You need to add headers that say "this is allowed." Browsers also send a preflight OPTIONS request before any POST/PUT/DELETE from another origin, so you need to handle that too:
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
return json(
{ data: 'value' },
{
headers: {
'Access-Control-Allow-Origin': '*'
}
}
);
};
// Preflight request — browsers send OPTIONS before cross-origin POST/PUT/DELETE
export const OPTIONS: RequestHandler = async () => {
return new Response(null, {
headers: {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type'
}
});
};⚠️ 'Access-Control-Allow-Origin': '*' allows any website to call your API. Fine for development, but in production you should restrict it to specific domains:
// Allow a single domain
'Access-Control-Allow-Origin': 'https://myapp.com'
// Allow multiple domains (check the request origin dynamically)
const allowed = ['https://myapp.com', 'https://admin.myapp.com'];
const origin = request.headers.get('origin');
'Access-Control-Allow-Origin': allowed.includes(origin) ? origin : ''Key Takeaways
+server.tsfiles define API endpoints — no+page.svelteneeded- Export
GET,POST,PUT,DELETEetc. as named functions - Use
json()helper for JSON responses,new Response()for anything else error()throws HTTP error responses- Same dynamic routing (
[param]) as pages - These run server-side only — safe for DB access and secrets