API Routes with +server.js
Building JSON endpoints and webhooks with +server.js, for clients other than your own pages.
읽는 데 3분
load functions and form actions cover data for your own pages, but sometimes you need an endpoint that isn't tied to rendering a page at all — a JSON API consumed by a mobile app, a webhook receiver, or an endpoint your own client-side code polls. That's what +server.js is for.
A basic GET endpoint
// src/routes/api/posts/+server.js
import { json } from '@sveltejs/kit';
import { db } from '$lib/server/database';
export async function GET() {
const posts = await db.query('SELECT id, title FROM posts ORDER BY created_at DESC');
return json(posts);
}Visiting /api/posts in a browser, or calling it with fetch, returns a JSON response. The exported function names — GET, POST, PUT, DELETE, PATCH — map directly to HTTP methods; a +server.js only needs to export the ones it supports, and SvelteKit returns a 405 automatically for the rest.
A POST endpoint with a request body
// src/routes/api/posts/+server.js
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/database';
export async function POST({ request, locals }) {
if (!locals.user) {
error(401, 'Not authenticated');
}
const { title, body } = await request.json();
const post = await db.insert('posts', { title, body, author_id: locals.user.id });
return json(post, { status: 201 });
}This looks almost identical to a form action, and that's intentional — both live in server-only files and share the same request context (locals, cookies, url). The difference is audience: use a form action when a page's own form is the client; use +server.js when the caller is something else — external code, a fetch call from a component that isn't submitting a form, or a third-party webhook.
+server.js and +page.svelte can't coexist
A given route folder is either a page (+page.svelte) or a raw endpoint (+server.js) — not both at the same path. To have a page and a JSON API for the same resource, give the endpoint its own path, commonly under /api:
src/routes/
├── posts/
│ └── +page.svelte → HTML page at /posts
└── api/
└── posts/
└── +server.js → JSON endpoint at /api/posts
Handling a webhook
Because +server.js gives you the raw Request, it's also the right place for things like verifying a webhook signature — logic that doesn't fit the page/form model at all:
// src/routes/webhooks/stripe/+server.js
import { error, json } from '@sveltejs/kit';
import { STRIPE_WEBHOOK_SECRET } from '$env/static/private';
export async function POST({ request }) {
const signature = request.headers.get('stripe-signature');
const body = await request.text();
if (!verifySignature(body, signature, STRIPE_WEBHOOK_SECRET)) {
error(400, 'Invalid signature');
}
const event = JSON.parse(body);
// ...handle event.type...
return json({ received: true });
}Choosing the right tool
A useful rule of thumb: if the data is for rendering a page, use load. If it's submitted by a form on a page, use a form action. If it's consumed by something that isn't a SvelteKit page at all — another service, a mobile client, your own client-side polling code — reach for +server.js. All three share the same server-only primitives underneath; the difference is purely about who's calling.