Svelte logo

SvelteKit by Example: API Endpoints

API endpoints return data instead of rendering a page. Create a +server.js file and export a handler for each HTTP method you want to support.

In this example, we'll read and update a public demonstration todo list.

Provide your own database module in src/lib/server/db.js. Its async getTodos() returns an array of { id, task, completed } objects with string IDs. createTodo(task) saves and returns a new todo with completed: false. deleteTodo(id) returns whether a todo was deleted, and getTodo(id) returns a todo or null. These are application helpers, not SvelteKit APIs.

These request-time database reads and writes need a server adapter and runtime, not static hosting. This documentation only displays the code. For real private todos, authenticate every request and check ownership before reading or changing data.

Keep database code in a server-only module. Use the #lib package import configured in the routing example.

GET handles requests to /api/todos. Response.json creates a response with an application/json content type.

src/routes/api/todos/+server.js
import { getTodos } from '#lib/server/db.js';





/** @type {import('./$types').RequestHandler} */
export async function GET() {
  return Response.json(await getTodos());
}

Add these imports and this handler to the same file. POST creates a todo; the client sends { task } as JSON.

Reject other content types and malformed JSON before validating the task.

Validate on the server, even when the browser also checks the input.

Return the saved todo with status 201 Created.

src/routes/api/todos/+server.js
import { createTodo } from '#lib/server/db.js';
import { error } from '@sveltejs/kit';

/** @type {import('./$types').RequestHandler} */
export async function POST({ request }) {
  const type = request.headers.get('content-type') ?? '';



  if (type.split(';')[0].trim() !== 'application/json') {
    error(415, 'Send JSON');
  }

  let body;
  try {
    body = await request.json();
  } catch {
    error(400, 'Invalid JSON');
  }

  const task = typeof body?.task === 'string' ? body.task.trim() : '';

  if (!task || task.length > 200) {
    error(400, 'Enter a task of 1–200 characters');
  }

  const todo = await createTodo(task);




  return Response.json(todo, { status: 201 });
}

A dynamic endpoint receives its ID through params, just like a page load function.

Return 204 No Content after deleting. There's no response body to parse.

src/routes/api/todos/[id]/+server.js
import { deleteTodo } from '#lib/server/db.js';
import { error } from '@sveltejs/kit';

/** @type {import('./$types').RequestHandler} */
export async function DELETE({ params }) {
  if (!await deleteTodo(params.id)) {
    error(404, 'Todo not found');
  }



  return new Response(null, { status: 204 });
}

Other methods work the same way: export PATCH for partial updates, PUT for replacement, or OPTIONS for supported request options. Unsupported methods receive a 405 Method Not Allowed response. When there's a GET handler but no HEAD, SvelteKit handles HEAD using GET without returning its body.

Use form actions for page forms that should work without JavaScript. Use endpoints for JSON clients or other consumers of your API. We'll call this API and refresh the list in the refreshing data example.