بنقرة واحدة
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)