API Guide
Interactive docs are also available at /docs (Swagger UI) and /redoc (ReDoc). The full Markdown source is below.
# INF601 Practice Hub — REST API Reference
This is the API you will practice against all semester. You will:
1. **Weeks 2–3:** call it with Python `requests` (Mini Project 1).
2. **Weeks 4–5:** optionally pull practice data from it for NumPy/Pandas projects.
3. **Week 12:** wrap it in your own **MCP server** so Claude Code can manage your
content for you (Mini Project 7).
Interactive versions of this reference are always available on the running server:
- **Swagger UI:** `http://127.0.0.1:8000/docs` — try every endpoint in the browser
- **ReDoc:** `http://127.0.0.1:8000/redoc` — clean reading view
- **OpenAPI JSON:** `http://127.0.0.1:8000/openapi.json` — the machine-readable spec
> Replace `http://127.0.0.1:8000` with the course-hosted URL your instructor gives you.
---
## Authentication
Every endpoint except `register`, `token`, and `health` requires an **API token**.
Send it in the `Authorization` header as a Bearer token:
```
Authorization: Bearer <your_api_token>
```
You get a token by registering once. Keep it secret — anyone with your token can
act as you. Store it in an environment variable, **never** commit it to GitHub.
```bash
export PRACTICE_API_TOKEN="paste-your-token-here"
```
### Status codes you will see
| Code | Meaning | When |
|------|---------|------|
| 200 | OK | Successful GET / PUT / PATCH |
| 201 | Created | Successful POST (register, create post/comment) |
| 204 | No Content | Successful DELETE |
| 401 | Unauthorized | Missing/invalid token, or wrong password |
| 403 | Forbidden | You tried to edit/delete a post that isn't yours |
| 404 | Not Found | No post/comment/dataset with that id/name |
| 409 | Conflict | Email already registered |
| 422 | Unprocessable Entity | Body failed validation (missing field, too short, wrong type) |
All errors return the same JSON shape so your code can handle them uniformly:
```json
{ "detail": "human-readable message" }
```
---
## Endpoints
### `POST /api/v1/auth/register` — create an account
Creates a user and returns your API token. No auth required.
**Request body**
```json
{ "name": "Jane Doe", "email": "jane@example.com", "password": "password123" }
```
`password` must be at least 6 characters. Returns **201** with:
```json
{ "api_token": "kЗ9...long-random-string...", "token_type": "bearer" }
```
**curl**
```bash
curl -X POST http://127.0.0.1:8000/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"Jane Doe","email":"jane@example.com","password":"password123"}'
```
**Python `requests`**
```python
import requests
BASE = "http://127.0.0.1:8000"
resp = requests.post(f"{BASE}/api/v1/auth/register", json={
"name": "Jane Doe",
"email": "jane@example.com",
"password": "password123",
})
resp.raise_for_status()
token = resp.json()["api_token"]
```
---
### `POST /api/v1/auth/token` — re-issue your token
If you lose your token (or want to rotate it), exchange your email + password for
a fresh one. **This invalidates your previous token.** No auth header required.
```json
{ "email": "jane@example.com", "password": "password123" }
```
Returns **200** with a new `api_token`. Wrong credentials → **401**.
```python
token = requests.post(f"{BASE}/api/v1/auth/token", json={
"email": "jane@example.com", "password": "password123",
}).json()["api_token"]
```
---
### `GET /api/v1/me` — who am I?
Returns the account that owns the supplied token. Handy for confirming auth works.
```python
headers = {"Authorization": f"Bearer {token}"}
me = requests.get(f"{BASE}/api/v1/me", headers=headers).json()
# {"id": 1, "name": "Jane Doe", "email": "jane@example.com", "created_at": "..."}
```
---
### `GET /api/v1/posts` — list posts
Returns up to `limit` posts, newest first. Query parameters (all optional):
| Param | Type | Default | Meaning |
|--------|------|---------|---------|
| `mine` | bool | false | Only your own posts |
| `author` | int | — | Posts by this user id |
| `tag` | str | — | Posts containing this tag |
| `limit` | int | 50 | Page size (1–100) |
| `offset` | int | 0 | Skip this many (pagination) |
```python
# All posts tagged "python" that belong to me:
posts = requests.get(
f"{BASE}/api/v1/posts",
headers=headers,
params={"mine": True, "tag": "python"},
).json()
```
Each post looks like:
```json
{
"id": 1, "title": "Hello", "body": "...", "tags": ["python", "api"],
"author_id": 1, "author_name": "Jane Doe",
"created_at": "2026-01-15T12:00:00", "updated_at": "2026-01-15T12:00:00"
}
```
---
### `GET /api/v1/posts/{id}` — get one post
Returns the post or **404** if it doesn't exist.
```python
post = requests.get(f"{BASE}/api/v1/posts/1", headers=headers).json()
```
---
### `POST /api/v1/posts` — create a post
You become the owner. Returns **201** with the created post.
```json
{ "title": "My Post", "body": "Markdown is fine here.", "tags": ["python", "api"] }
```
Only `title` is required (`body` defaults to empty, `tags` to none).
```python
new_post = requests.post(f"{BASE}/api/v1/posts", headers=headers, json={
"title": "My Post",
"body": "Hello world",
"tags": ["python"],
}).json()
post_id = new_post["id"]
```
---
### `PUT` / `PATCH /api/v1/posts/{id}` — update a post
**Owner only** — editing someone else's post returns **403**.
`PATCH` updates the fields you send; `PUT` is treated the same way (send all fields).
```python
updated = requests.patch(
f"{BASE}/api/v1/posts/{post_id}",
headers=headers,
json={"title": "My Post (edited)"},
).json()
```
---
### `DELETE /api/v1/posts/{id}` — delete a post
**Owner only.** Returns **204** (no body) on success, **403** if it isn't yours,
**404** if it doesn't exist.
```python
resp = requests.delete(f"{BASE}/api/v1/posts/{post_id}", headers=headers)
assert resp.status_code == 204
```
---
### `GET` / `POST /api/v1/posts/{id}/comments` — comments
- `GET` lists a post's comments (oldest first).
- `POST` adds a comment — body: `{ "body": "Nice post!" }` — returns **201**.
```python
requests.post(
f"{BASE}/api/v1/posts/{post_id}/comments",
headers=headers,
json={"body": "Great work!"},
)
```
**Timed check-in posts.** Some posts are "check-ins" that accept comments **only
within a time window**. If you `POST` a comment too early or too late, you get
**`423 Locked`** and nothing is saved. The window itself is *not* shown in the
API — you recognize a check-in by the word **`check-in`** in its **title**, reply
while it is open, and handle the `423` if it is closed. (Used by the Scheduled
Check-In Bot assignment.)
---
### `GET` / `POST /api/v1/posts/{id}/attachments` — files on a post
- `GET` lists a post's attachments (metadata only).
- `POST` uploads a file (multipart, owner only) — returns **201**. Max 5 MB.
Each post you read also carries an `attachments` list, where every entry has a
`download_url`. To capture *all* of a post's content, follow it:
```python
post = requests.get(f"{BASE}/api/v1/posts/{post_id}", headers=headers).json()
for att in post["attachments"]:
blob = requests.get(f"{BASE}{att['download_url']}", headers=headers).content
with open(att["filename"], "wb") as fh:
fh.write(blob)
```
### `GET /api/v1/attachments/{id}` — download an attachment
Returns the raw file bytes with its content type. Auth required.
---
### `GET /api/v1/datasets/{name}` — practice data
Generates **deterministic** fake data for the NumPy/Pandas projects. Same `count`
always returns the same rows, so your charts are reproducible. Names:
- `stocks` — `ticker, date, open, close, volume`
- `people` — `name, email, city, age, job`
- `movies` — `title, director, year, genre, rating`
Query param `count` (1–500, default 10). Unknown name → **404**.
```python
data = requests.get(
f"{BASE}/api/v1/datasets/stocks",
headers=headers,
params={"count": 10},
).json()
rows = data["rows"] # list of dicts, ready for pandas.DataFrame(rows)
```
---
### `GET /health` — liveness check
Returns `{"status": "ok"}`. No auth. Use it to confirm the server is up.
---
## A complete round-trip (copy/paste starter)
```python
import os
import requests
BASE = "http://127.0.0.1:8000"
# 1. Register once (or reuse a saved token).
token = requests.post(f"{BASE}/api/v1/auth/register", json={
"name": "Jane Doe", "email": "jane@example.com", "password": "password123",
}).json()["api_token"]
headers = {"Authorization": f"Bearer {token}"}
# 2. Create.
post = requests.post(f"{BASE}/api/v1/posts", headers=headers, json={
"title": "Round trip", "body": "created via API", "tags": ["demo"],
}).json()
# 3. Read.
print(requests.get(f"{BASE}/api/v1/posts/{post['id']}", headers=headers).json())
# 4. Update.
requests.patch(f"{BASE}/api/v1/posts/{post['id']}", headers=headers,
json={"title": "Round trip (edited)"})
# 5. Delete.
requests.delete(f"{BASE}/api/v1/posts/{post['id']}", headers=headers)
```