Skip to main content

Backend API Handover for Frontend Team 🚀

Welcome! This document provides everything you need to start building the frontend application against the new backend API for the Evaluations project.

The backend provides a versioned API for managing questions. The core workflow is creating a "parent" question, adding one or more "versions" (the actual content) to it, and then "publishing" a specific version to make it active.

1. The API Contract: OpenAPI Specification

The single source of truth for the API's structure, endpoints, and data models is the OpenAPI specification.

  • File: openapi/dist/openapi.bundle.yaml
  • Purpose: This bundled file contains the entire API definition. You can use it for:
    • reference: To look up any endpoint, parameter, or response schema.
    • Code Generation: To automatically generate a fully typed API client (see "Next Steps" below).
    • Tooling: To import into tools like Postman or Insomnia to create a request collection instantly.

2. Getting Started Quickly: A Simple API Client

To help you get started without writing boilerplate code, we've created a lightweight, zero-dependency API client. This wrapper handles making requests, setting headers, and processing responses and errors.

Location: We recommend placing this file at src/lib/api/client.ts in your SvelteKit project.

// src/lib/api/client.ts
import { PUBLIC_API_BASE } from '$env/static/public';

const BASE = (PUBLIC_API_BASE ?? '').replace(/\/+$/, '');

/**
* A helper function to process the JSON response and handle errors.
* The backend returns \{ error, message } on failure, which this function throws.
*/
async function asJson<T>(res: Response): Promise<T> {
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw data;
}
return data as T;
}

export const api = {
/**
* Checks the health of the API.
*/
health() {
return fetch(`\${BASE}/api/v1/health`).then(asJson<{ status: string; uptime: number }>);
},

/**
* Lists all question parents.
*/
listQuestions(params: {
page?: number;
limit?: number;
q?: string;
qtype?: "mcq_single" | "mcq_multi" | "short_text";
contributes_to_score?: boolean;
tags_all?: string; // Comma-separated
tags_any?: string; // Comma-separated
} = {}) {
const qs = new URLSearchParams();
Object.entries(params).forEach(([k, v]) => v !== undefined && qs.append(k, String(v)));
return fetch(`\${BASE}/api/v1/questions?\${qs}`).then(asJson);
},

/**
* Creates a new question parent.
*/
createQuestion(body: { status: "draft" | "in_review" | "published" | "archived"; tags?: string[] }) {
return fetch(`\${BASE}/api/v1/questions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).then(asJson);
},

/**
* Creates a new version for a specific question.
*/
createVersion(id: string, body: unknown) {
return fetch(`\${BASE}/api/v1/questions/$\{id\}/versions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).then(asJson);
},

/**
* Publishes a specific version of a question, making it the active one.
*/
publishVersion(id: string, activeVersionId: string) {
return fetch(`\${BASE}/api/v1/questions/$\{id\}/publish`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ activeVersionId }),
}).then(asJson);
},

/**
* Updates the metadata of a question parent (e.g., its status or tags).
*/
updateQuestion(id: string, body: Partial<{ status: "draft" | "in_review" | "published" | "archived"; tags: string[]; ifUpdatedAt: number }>) {
return fetch(`\${BASE}/api/v1/questions/$\{id\}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).then(asJson);
},

/**
* Deletes a question parent and all its versions.
*/
deleteQuestion(id: string) {
return fetch(`\${BASE}/api/v1/questions/$\{id\}`, { method: "DELETE" });
},
};

How to Use It (SvelteKit Example)

<script lang="ts">
import { onMount } from 'svelte';
import { api } from '$lib/api/client';

let questionsPromise = api.listQuestions({ limit: 5 });

async function createNewQuestion() {
try {
const newQuestion = await api.createQuestion({ status: 'draft', tags: ['new'] });
alert(`Created question with ID: \${newQuestion.id}`);
// Refresh the list
questionsPromise = api.listQuestions({ limit: 5 });
} catch (err) {
alert(`Error: \${err.message}`);
}
}
</script>

<h1>Questions</h1>
<button on:click={createNewQuestion}>Create New Question</button>

{#await questionsPromise}
<p>Loading...</p>
{:then response}
<ul>
{#each response.items as question (question.id)}
<li>ID: {question.id} | Status: {question.status}</li>
{/each}
</ul>
{:catch error}
<p style="color: red;">Failed to load questions: {error.message}</p>
{/await}

3. Test Payloads: JSON Examples

To help with testing, here are some "known-good" JSON payloads that you can use when calling the API client functions.

✅ Success Case: Creating a Question Parent

{
"status": "draft",
"tags": ["go", "easy"]
}

✅ Success Case: Creating a Question Version (MCQ)

This is a valid body for the api.createVersion(questionId, body) function.

{
"qtype": "mcq_single",
"contributesToScore": true,
"payload": {
"defaultLocale": "en-gb",
"locales": {
"en-gb": {
"type": "mcq_single",
"stem": "What is a goroutine?",
"choices": [
{ "id": "c1", "text": "A lightweight thread" },
{ "id": "c2", "text": "A function pointer" }
],
"answer": { "correctIds": ["c1"] },
"grading": { "mode": "all-or-nothing", "weight": 1 }
}
}
}
}

❌ Error Case: Testing Validation

The backend performs validation. For example, sending a payload with duplicate choice IDs to api.createVersion() will result in a 422 Unprocessable Entity error. The client.ts helper will throw the JSON body of the error response.

Example Bad Payload (Duplicate id: "c1")

{
"qtype": "mcq_single",
"payload": {
"defaultLocale": "en-gb",
"locales": {
"en-gb": {
"type": "mcq_single",
"stem": "This will fail",
"choices": [
{ "id": "c1", "text": "First choice" },
{ "id": "c1", "text": "Duplicate ID choice" }
]
}
}
}
}

Expected Error Caught in UI

{
"error": "validation_error",
"message": "Payload for locale 'en-gb' contains duplicate choice IDs: [c1]."
}

4. Local Development Setup

Browser-facing frontend code should usually leave PUBLIC_API_BASE unset so requests stay same-origin (/api/...) on the current app origin. If you intentionally need to target the published dev API directly, create a .env file at the root of your SvelteKit project with the following content:

PUBLIC_API_BASE=https://api-dev.evalium.me

If PUBLIC_API_BASE is unset, the client.ts helper defaults to same-origin requests.


5. Next Steps & Future Improvements (Optional)

This setup is designed to get you running immediately. As the project grows, we can improve the workflow:

  • Fully Typed Client: We can use a tool to generate a TypeScript interface for the entire API from the openapi.bundle.yaml file, giving you full type safety and autocompletion.
  • API Mocking: You can use a tool like Prism (prism mock openapi/dist/openapi.bundle.yaml) to run a mock server based on the OpenAPI spec, allowing you to develop the UI without a running backend.

If you have any questions, please reach out!