원클릭으로
api-endpoint
Critical instructions on how to create and test SvelteKit API endpoints
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Critical instructions on how to create and test SvelteKit API endpoints
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
SQLite Database skill for LD's admin shared.db + per-dict dictionaries/<id>.db (content) — how to WRITE DB/sync/schema/migration code AND how to READ/query the live local and production VPS databases (user/dictionary lookups, schema state, edits). wa-sqlite in browsers + better-sqlite3 on the VPS, sync engines, schema, migrations, queries, live reactive UI data.
Log in as any user — site admin, super manager, dictionary manager/editor/contributor, or plain visitor — in dev/e2e browser testing without a real inbox or hand-minted tokens. Read before any puppeteer/curl session that needs an authenticated LD user.
Read the LD client_logs telemetry — browser errors/crashes/sessions AND server-side events — to debug an issue, see what a user did, or verify telemetry. Read this whenever you need to look at the logs for ANY reason, in local dev (site/.data/logs.db) or production (the living VPS).
Query the live browser wa-sqlite DBs (admin shared.db + per-dict dict.db) via the local dev proxy. Use to inspect or debug local data — dirty rows, sync watermarks, entries/senses, catalog, messages, users — without opening DevTools.
UI design guidelines (CSS, icons, theme) and svelte-look component stories for visual screenshot verification. Read when building or modifying the SvelteKit web app components.
Svelte 5 and SvelteKit documentation lookup and code analysis. Use when writing or debugging Svelte components.
| name | api-endpoint |
| description | Critical instructions on how to create and test SvelteKit API endpoints |
When creating, modifying, or testing SvelteKit API endpoints (+server.ts files) in site/.
_call filesEvery API endpoint has a companion _call.ts (or _call.svelte.ts). Client-side code must never call fetch('/api/...') directly — always import and call the function from the _call file. This gives you:
post_request / get_requestsession cookie — no header wranglingInterface names are derived by PascalCasing the route path segments after api/. For routes/api/foo/bar/+server.ts → FooBar, routes/api/me/profile/+server.ts → MeProfile, etc:
import type { RequestHandler } from './$types'
import { verify_auth } from '$lib/auth/verify'
import { ResponseCodes } from '$lib/constants'
import { error, json } from '@sveltejs/kit'
export interface FooBarRequestBody {
email: string
}
export interface FooBarResponseBody {
result: 'success'
}
export const POST: RequestHandler = async (event) => {
// auth-gate endpoints that hit the DB or perform privileged actions
const { user_id } = await verify_auth(event)
const { email } = await event.request.json() as FooBarRequestBody
if (!email)
error(ResponseCodes.BAD_REQUEST, 'No email provided')
try {
// ... your logic here
return json({ result: 'success' } satisfies FooBarResponseBody)
} catch (err) {
console.error(`Error: ${(err as Error).message}`)
error(ResponseCodes.INTERNAL_SERVER_ERROR, `Error: ${(err as Error).message}`)
}
}
Key details:
ResponseCodes from $lib/constants (OK, BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, INTERNAL_SERVER_ERROR, …) instead of magic numbers.satisfies when returning JSON for type-checking.event (RequestEvent) — pass it to verify_auth(event) directly, then use event.request.json() for the body.Auth is cookie-first with an Authorization: Bearer fallback. Use the shape that matches the gate:
verify_auth — any logged-in userimport { verify_auth } from '$lib/auth/verify'
const { user_id, email, name } = await verify_auth(event)
// throws 401 if the session cookie / bearer token is missing or invalid
verify_auth(event) reads the JWT from the httpOnly session cookie first, then falls back to Authorization: Bearer <JWT> (for non-browser callers). Returns { user_id, email, name }.
verify_auth_dict_role — per-dictionary contributor / manager actionsimport { verify_auth_dict_role } from '$lib/auth/verify-dict-role'
const { user_id, email, role } = await verify_auth_dict_role(event, { dictionary, min_role: 'manager' })
// throws 401 on no auth, 403 on missing/insufficient role
// role: 'contributor' | 'manager' | 'admin' (LD has no 'editor' role)
Site admins (admin_level >= 1, derived from site/src/lib/admins.ts) bypass the per-dict role check entirely. The fresh DB lookup on every push ensures revocations are immediate — JWT-baked roles would let revoked editors keep pushing until token expiry.
_call.ts filePlace alongside +server.ts. Auth rides automatically on the session cookie — the _call file just passes business data:
// routes/api/foo/bar/_call.ts
import type { FooBarRequestBody, FooBarResponseBody } from './+server'
import { post_request } from '$lib/utils/requests'
export async function api_foo_bar(body: FooBarRequestBody) {
return await post_request<FooBarRequestBody, FooBarResponseBody>('/api/foo/bar', body)
}
Callers get { data, error }:
const { data, error } = await api_foo_bar({ email: 'user@example.com' })
if (error) {
console.error(error.message)
return
}
console.log(data.result)
post_request / get_request in $lib/utils/requests.ts:
content-type: application/json, JSON-serialize the body, parse the response{ data: null, error: { status, message } }fetch, so the browser sends the httpOnly session cookie automatically — no Authorization header attachment neededTest +server.ts directly: synthesize a { request, cookies } event, sign a real JWT for the auth path, call the handler, and assert on the response AND the DB side-effect. Run against an in-memory shared.db so each test is isolated and the real migration runner exercises the real schema. Don't mock verify_auth — that hides bugs in the auth layer.
src/routes/api/foo/bar/
├── +server.ts # Endpoint handler (TEST THIS directly)
├── _call.ts # Client function (thin; no separate test needed)
└── server.test.ts # Tests against +server.ts
api/auth/update-profile/server.test.ts)import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
import { sign_jwt } from '$lib/auth/jwt'
import { open_shared_db } from '$lib/db/server/shared-db'
import { POST } from './+server'
let db: ReturnType<typeof open_shared_db>
// Swap the server's shared-db singleton for a per-test in-memory DB
vi.mock('$lib/db/server/shared-db', async () => {
const actual = await vi.importActual<typeof import('$lib/db/server/shared-db')>('$lib/db/server/shared-db')
return { ...actual, get_shared_db: () => db }
})
beforeAll(() => {
process.env.JWT_SECRET = 'test-secret-that-is-long-enough-for-hs256'
})
beforeEach(() => {
db = open_shared_db(':memory:') // runs all shared-migrations
db.prepare('INSERT INTO users (id, email, name, providers, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)')
.run('user-1', 'user@example.com', 'Old Name', JSON.stringify([{ provider: 'email', provider_id: 'user@example.com' }]), '2025-01-01T00:00:00Z', '2025-01-01T00:00:00Z')
})
afterEach(() => {
db.close()
})
// Auth via a REAL signed JWT on the session cookie — exercises the real verify_auth
function call(body: unknown, options: { token?: string } = {}) {
const request = new Request('http://localhost/api/foo/bar', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
})
const cookies = { get: (name: string) => (name === 'session' ? options.token : undefined) }
return POST({ request, cookies } as unknown as Parameters<typeof POST>[0])
}
async function token(user_id = 'user-1', email = 'user@example.com') {
return sign_jwt({ sub: user_id, email, name: 'Old Name' })
}
describe(POST, () => {
test('401 without auth', async () => {
await expect(call({ email: 'x@y.com' })).rejects.toMatchObject({ status: 401 })
})
test('400 on bad input', async () => {
await expect(call({}, { token: await token() })).rejects.toMatchObject({ status: 400 })
})
test('happy path', async () => {
const response = await call({ email: 'x@y.com' }, { token: await token() })
expect(response.status).toBe(200)
expect(await response.json()).toEqual({ result: 'success' })
})
})
error() from @sveltejs/kit throws, so assert rejections with .rejects.toMatchObject({ status }).
SELECT from the in-memory DB to confirm the write landed)Inline for small payloads, file for large:
expect(data).toMatchInlineSnapshot(`{ "result": "success" }`)
expect(big).toMatchFileSnapshot('./snapshots/response.json')
// Update with: pnpm --filter=site test -u
open_shared_db(':memory:') AND/OR a per-dict :memory: connection — see src/routes/api/dictionary/[id]/db/server.test.ts for the per-dict shape.dictionary_roles row in the in-memory shared.db.sveltekit-endpoint-helper in site/ yet — tests synthesize { request, cookies } directly. If a helper would simplify a batch of new tests, port one from house/tutor at that point.src/routes/api/auth/update-profile/ — simplest auth+update pattern (shared.db row update)src/routes/api/dictionaries/create/ — auth + insert + uniqueness conflict (409)src/routes/api/dictionaries/[id]/roles/ — dict-role-gated, manages dictionary_rolessrc/routes/api/dictionary/[id]/db/ — per-dict push (write to dict.db, verify_auth_dict_role)src/routes/api/dictionary/[id]/changes/ — per-dict pull (snapshot-aware delta)src/routes/api/messages/reply/ — admin-only (is_admin(email) check), TipTap → email-outsrc/routes/api/messages/assign/ — admin-only with ntfy push side-effect (silenced via NTFY_DISABLED=1 in tests)site/src/lib/auth/verify.ts, verify-dict-role.ts, jwt.ts; admin allow-list: site/src/lib/admins.tssite/src/lib/constants.ts (ResponseCodes)site/src/lib/utils/requests.tssite/src/lib/db/server/shared-db.ts (open_shared_db, get_shared_db)