Skip to content
MatsHaugum._

GET /projects/clinicbook 200 OK

ClinicBook

Year-2 exam project — Noroff, 2026

ClinicBook is my year-2 exam project: a booking system for medical clinics, built as an ASP.NET Core REST API with a React front end on top. I wanted the booking itself to stay simple enough that a patient never has to make an account, while still giving clinic staff a real admin surface behind it.

  • C#
  • ASP.NET Core
  • EF Core
  • SQL Server
  • Redis
  • React
  • TypeScript
  • JWT
  • Refresh Tokens
  • Integration Testing
  • Docker
  • Caddy
  • Cloudflare Tunnel
  • GitHub Actions

Booking flow

A patient picks a clinic, a specialty, and a time slot, then confirms. Booking as a guest takes an email and nothing more — no account, no password.

Registering is optional and only buys you one thing: a place to see and manage the appointments you've already made. That kept the flow short for the common case without cutting off the people who want to come back to it.

Booking as a guest, start to how to reschedule — silent screencast · download (WebM)
The confirmation screen showing the booked clinic, doctor, specialty, date, and time.
Patient appointments

Admin panel

Clinic staff manage doctors, clinics, specialties, and appointment categories from one panel. These are the records the booking flow reads from, so adding a doctor or a new specialty changes what patients can book without a deploy.

Login to admin panel. Managing doctors, clinics, specialties, and categories — silent screencast · download (WebM)
The admin doctors table, listing each doctor with their specialty and clinic.
The doctors table

Auth & API

The API issues short-lived JWT access tokens with refresh token rotation behind them, so a stolen access token stops being useful quickly and a refresh token can only be spent once.

A sequence diagram between the browser, the API, and SQL Server: login returns an access JWT and a refresh token, a later request fails with 401, the response interceptor posts the refresh token, the API validates and revokes it, issues a new pair, and the original request is retried automatically.
The refresh cycle: a 401 is caught, tokens are rotated, and the request is retried

Self-hosted architecture & deploys

The live demo runs on my own Ubuntu server with no inbound ports open. A Cloudflare Tunnel makes an outbound-only connection to Cloudflare's edge, so the router forwards nothing and the home IP never ends up in DNS. Behind the tunnel an edge Caddy routes by URL path across the projects I host and strips the /projects/clinicbook prefix, then hands off to the app's own Caddy, which serves the React build and proxies /api/* to the ASP.NET Core API on Kestrel.

SQL Server 2022 and Redis 7 run as containers on an internal Docker network with no internet access — nothing reaches them except the API. Redis is the distributed cache in front of the read-heavy endpoints: doctor search results are cached for 30 seconds and keyed on the query string, so repeated searches don't hit the database.

Testing and deploying are both automatic. Every push to main runs the integration suite in GitHub Actions — xUnit driving the full HTTP pipeline through WebApplicationFactory against a real SQL Server and a real Redis, so what the tests exercise is the same stack that runs in production, not mocks. Deploys are gated on that run: when CI finishes, GitHub sends an HMAC-signed workflow_run webhook to the server, and it only redeploys if the run succeeded on a push to main — then it pulls and rebuilds the Compose stack at the exact commit that passed. A red build never reaches production, and shipping is still just a push; I never SSH in to release.

A flowchart of the request path: browser to Cloudflare edge (TLS, CDN, WAF), to a cloudflared tunnel making an outbound-only connection, to the edge Caddy that routes by path and strips /projects/clinicbook, to the app Caddy serving the React SPA and proxying /api/*, to the ASP.NET Core API on Kestrel port 8080. The API talks to SQL Server 2022 and Redis 7 on an internal Docker network with no internet access. A GitHub push sends an HMAC-signed webhook to the edge Caddy.
Request path in, HMAC-signed deploy webhook on the side — no open inbound ports