Repository navigation
API Catalog
The index of the HTTP API: base URL, auth model, error contract, conventions, and every live route in one table. Status: current. Only routes marked ✅ exist; shapes live on the per-resource pages.
The backend is an Elysia app running on Bun.
| Environment | Base URL | Notes |
|---|---|---|
| Production | https://tts.niaefeup.pt/api/v1 |
Niployments mounts the service under /api
|
| Local | http://localhost:3000/v1 |
PUBLIC_API_PREFIX is empty outside production |
The /api segment is added by the deployment, not declared in the code. Routes are declared under
/v1 in apps/backend/src/interface/routes/, so a route written as /v1/courses is
/api/v1/courses in production and /v1/courses locally.
GET / and /health/ are the only paths outside /v1. The legacy compatibility routes are mounted
at the root as well (see Auth API).
The frontend resolves the same base in apps/frontend/src/api/client.ts:
VITE_APP_BACKEND_URL if set, otherwise http://localhost:3000 in dev and
${window.location.origin}/api in a production build.
Auth is a session cookie, never a JWT or bearer token. The full flow, cookie attributes and response shapes are in Auth API.
-
GET /v1/auth/loginredirects to SIGARRA's OIDC provider. - SIGARRA redirects back to
GET /v1/auth/callback, which sets anHttpOnly,SameSite=Lax,Secure(per config)sessioncookie and redirects to the frontend. - Every later request sends that cookie.
POST /v1/auth/logoutdeletes both the server-side session and the cookie.
| Requirement | Routes |
|---|---|
Session required — anonymous callers get 401 unauthorized before the handler runs |
/v1/faculties/*, /v1/courses/*, /v1/course-units/*
|
Session optional — answers { signed: false } when anonymous |
/v1/auth/me, /v1/auth/logout, /auth/info/, /oidc-auth/logout/
|
| Public |
/, /health/, /v1/auth/login, /v1/auth/callback, /v1/auth/dev-login (dev only), /oidc-auth/authenticate/, /sigarra_login/ (dev only) |
There are no role gates on any implemented route. isAdmin and role are exposed on the session,
but admin-only endpoints do not exist yet.
Dev login is enabled by
DEV_LOGIN_ENABLEDand is forced off in production. When disabled,POST /v1/auth/dev-loginandPOST /sigarra_login/answer404 not-foundinstead of403.
Every failure — thrown, validated, unmatched or unexpected — is normalized by the global onError
hook to the same body:
{ "error": "Faculty FEUP was not found", "code": "faculty-not-found" }-
codeis stable and machine-readable; branch on it. -
erroris a human-readable message, safe to show. - Every 5xx body is exactly
{ "error": "Something went wrong", "code": "<code>" }— the cause is logged, never returned.
| Status | code |
When |
|---|---|---|
400 |
route-specific (oidc-missing-code, …) |
The route decided the input is semantically wrong |
401 |
unauthorized, invalid-credentials, oidc-auth-error
|
No session, or login failed |
403 |
forbidden |
Reserved; no implemented route uses it |
404 |
not-found, faculty-not-found, course-not-found, course-unit-not-found
|
No route, or a well-formed id/acronym that does not exist |
409 |
conflict |
Reserved for the exchange lifecycle |
422 |
validation-error |
Body/query/path did not match the Elysia schema |
500 |
internal-error |
Bug, or a route failed its own response schema |
502 |
oidc-callback-failed |
SIGARRA token exchange failed |
503 |
oidc-not-configured, dev-login-not-configured
|
Required configuration is missing |
400 vs 422: a schema mismatch (e.g. ?year=abc, /:id/not-a-number, malformed JSON) is 422.
A 400 is only used when the route checks the value itself and rejects it.
- JSON in, JSON out. No form encoding, no XML.
-
Response schemas are enforced. Every route declares a
responseschema; Elysia strips fields the schema does not list. A response that fails its own schema becomes500 internal-error. -
Pagination is not implemented. No list endpoint accepts
page/limittoday. -
Versioning is the
/v1path segment. There is noAccept-Versionheader. - Localization is not implemented; every message is English.
-
Trailing slashes follow the declaration: catalog routes have none (
/v1/courses),/health/and the legacy compatibility routes keep theirs. -
OpenAPI is dev-only. With
NODE_ENV !== 'production', Scalar is served at/openapiand the document at/openapi/json.
19 routes are registered. Each group has its own page with shapes, examples and per-route errors.
| Status | Method | Path | Auth | Description |
|---|---|---|---|---|
| ✅ | GET |
/ |
public | Liveness greeting: { "message": "Hello from tts-be!" }
|
| ✅ | GET |
/health/ |
public | Health check: { "status": "ok" }
|
Auth — Auth API
| Status | Method | Path | Auth | Description |
|---|---|---|---|---|
| ✅ | GET |
/v1/auth/login |
public | Begin OIDC login; 302 to SIGARRA |
| ✅ | GET |
/v1/auth/callback |
public | OIDC return; sets the session cookie, 302 to the frontend |
| ✅ | GET |
/v1/auth/me |
optional | Current user or { "signed": false }
|
| ✅ | POST |
/v1/auth/dev-login |
public (dev only) | Username/password login for local development |
| ✅ | POST |
/v1/auth/logout |
optional | Deletes the session and clears the cookie |
| 🔁 | GET |
/oidc-auth/authenticate/ |
public | Legacy alias → /api/v1/auth/login; 302
|
| 🔁 | POST |
/oidc-auth/logout/ |
optional | Legacy logout |
| 🔁 | GET |
/auth/info/ |
optional | Legacy session shape (username, eligible_exchange, is_admin) |
| 🔁 | POST |
/sigarra_login/ |
public (dev only) | Legacy dev login |
| Status | Method | Path | Auth | Description |
|---|---|---|---|---|
| ✅ | GET |
/v1/faculties |
session | All faculties, ordered by acronym |
| ✅ | GET |
/v1/faculties/:acronym |
session | One faculty |
| ✅ | GET |
/v1/faculties/:acronym/courses |
session | Courses offered by a faculty; optional ?year=
|
| ✅ | GET |
/v1/courses |
session | All courses; optional ?year= and ?faculty=
|
| ✅ | GET |
/v1/courses/:id |
session | One course |
| ✅ | GET |
/v1/courses/:id/units |
session | Course units of a course |
| ✅ | GET |
/v1/course-units/:id |
session | One abstract course unit |
| ✅ | GET |
/v1/course-units/:id/occurrences |
session | Every occurrence of a unit, newest year first |
Per-resource pages: Faculties API · Courses API · Course units API.
These groups come from the API design document and are not registered. They are listed here only so nobody mistakes the design for a contract.
| Status | Group | Examples |
|---|---|---|
| 🚧 | Occurrences and classes |
GET /courses/:id/occurrences, GET /occurrences/:id/classes, GET /occurrences/hashes
|
| 🚧 | Classes and schedule slots | Classes, lesson types, slots, teacher assignment |
| 🚧 | Student / user data | Profile, current schedule, enrolled units, eligibility |
| 🚧 | Planner state |
GET/PUT /users/me/planner-state/:year — cross-device sync |
| 🚧 | Exchanges | Create, detail, message, claim, accept, delete, lifecycle |
| 🚧 | Enrollment requests | Student create, admin list and decide |
| 🚧 | Admin operations | Scoped courses/occurrences, exchange queue, statistics |
| 🚧 | Period management |
POST /occurrences/:id/periods, POST /courses/:id/periods
|
| 🚧 | Real-time collaboration | Socket.IO handshake and planner events |
| 🗑 | CSV export (server-side) | Dropped; the frontend exports CSV in the browser |
The full list counts 69 core endpoints + 3 compatibility routes = 72 designed, of which 9 auth routes, 8 catalog routes and 2 service routes are live.
Note:
endpoints.mdlists/courses/:id/occurrencesand/courses/:id/periods. Neither is implemented. Occurrences are only reachable today through/v1/course-units/:id/occurrences.
Anything that changes the wire contract must update, in the same pull request:
- the route and its schemas in
apps/backend/src/interface/routes/; - registration in
api.routes.tsand the auth middleware it mounts; - a
detailtag/summary for OpenAPI; - this index and the resource page;
- a route test under
apps/backend/tests/interface/routes/using the shared fakes; - the frontend Eden service, if the web app consumes it.
The step-by-step runbook is Adding an endpoint. The drift between this catalog
and /openapi/json is meant to be caught automatically — see
#930.
-
Glossary —
course,course unit,occurrence,class - What is TTS — how the API fits into the system
- Reading paths — the backend path
- API design document
- Error handling 🚧 · Adding an endpoint
- Planned endpoints 🚧 · Legacy route map 🚧
Last verified: 2026-10-27