07 - API Routes

📋 Jump to Takeaways

What 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/42params.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.ts files define API endpoints — no +page.svelte needed
  • Export GET, POST, PUT, DELETE etc. 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

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

© 2026 ByteLearn.dev. Free courses for developers. · Privacy