Student: Novikov Maxim IT3-2304
Course: Advanced Web Technologies · Lab 5: Frontend and Backend Integration
This project is the Next.js frontend of the Course Catalog application, integrated with the FastAPI backend. It demonstrates Server Components data fetching, Client Component data fetching via SWR, browser CORS requests, form validation against API schema, and preflight inspection.
Always start the backend first, then the frontend:
cd ../course-catalog-api
# Activate virtual environment
source .venv/bin/activate # macOS / Linux
# or .venv\Scripts\Activate.ps1 # Windows
# Run FastAPI dev server
fastapi dev main.pyThe API starts at http://127.0.0.1:8000.
cd ../course-catalog
# Install dependencies
pnpm install # or npm install
# Start Next.js development server
pnpm dev # or npm run devOpen http://localhost:3000 in your browser.
Next.js reads environment variables from .env.local. This file is git-ignored (.env* in .gitignore) so that sensitive secrets and environment-specific endpoints are not leaked into source control.
Create .env.local in the root of course-catalog:
# Server-side requests (Server Components, Route Handlers)
API_URL=http://localhost:8000
# Client-side requests (browser Client Components with SWR and fetch)
NEXT_PUBLIC_API_URL=http://localhost:8000API_URL: Visible only to code running on the Next.js server (e.g.getCourses()andgetCourse()inlib/courses.ts).NEXT_PUBLIC_API_URL: Inlined into client JavaScript bundles at build time for browser execution (e.g. inLikeButton.tsx,EnrollForm.tsx, andElectiveFilter.tsx).
generateStaticParams pre-renders dynamic routes at build time by querying the API during next build. If the backend is offline during build, the entire frontend build crashes (ECONNREFUSED). Furthermore, once the course catalog is managed by a real backend API, courses and like counts can change dynamically without rebuilding the frontend; pre-rendered static pages would show stale, frozen data. By removing generateStaticParams and using cache: "no-store", /courses and /courses/[id] become dynamic on-demand routes (ƒ), ensuring users always see up-to-date data while keeping the build independent of backend uptime.
The Like button sends a simple POST request to http://localhost:8000/courses/{id}/like without a request body and without custom headers. Under the W3C CORS specification, a POST request with a standard CORS-safelisted content type (or no body) is classified as a Simple Request and does not require a preflight check.
Conversely, the Enrollment form sends a JSON payload (POST /enrollments) with the request header Content-Type: application/json. Since application/json is not among the CORS-safelisted content types (application/x-www-form-urlencoded, multipart/form-data, text/plain), the browser must first send a preflight OPTIONS request to verify whether the server allows POST requests with a Content-Type header from http://localhost:3000. The server approves it with Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Max-Age: 600.
The following controlled breakages were executed and analyzed using the 5-step CORS diagnostic algorithm:
- What I did: Commented out the
app.add_middleware(CORSMiddleware, ...)block inmain.py. - Console:
Access to fetch at 'http://localhost:8000/courses' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. - Network:
/filter:GET coursesmarked red ((failed) net::ERR_FAILED/ CORS error), while backend terminal logged200 OK.- Like button:
POST .../likemarked red with CORS error, though backend terminal logged200 OKand server like counter incremented. - Enrollment form:
OPTIONS enrollmentsfailed with status405 Method Not Allowed;POST enrollmentswas blocked and never sent. /courses(Server Component): Worked normally with status200 OK.
- Cause: No CORS middleware was configured on FastAPI, so response headers lacked
Access-Control-Allow-Origin, causing the browser to block JavaScript from reading responses. Server components were unaffected because server-to-server requests bypass the browser SOP. - How I found it: Algorithm Step 3 (Response Headers lacked
Access-Control-Allow-Origin) and Step 4 (Preflight OPTIONS returned 405 Method Not Allowed).
- What I did: Changed
allow_origins=["http://localhost:3000"]toallow_origins=["http://localhost:3000/"]. - Console:
Access to fetch at 'http://localhost:8000/courses' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.and for the form:Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. - Network:
/filter:GET coursesfailed with CORS error.- Like button:
POST .../likefailed with CORS error. - Enrollment form:
OPTIONS enrollmentsreturned status400 Bad Requestwith response bodyDisallowed CORS origin.POSTwas never sent.
- Cause: Origins in CORS consist of
<scheme>://<host>:<port>with no path and strictly no trailing slash."http://localhost:3000/"does not match the browser's origin"http://localhost:3000". - How I found it: Algorithm Step 3 (No
Access-Control-Allow-Origin) and Step 4 (OPTIONS returned 400 Bad Request with bodyDisallowed CORS origin).
- What I did: Removed
allow_methodsor left default (onlyGETallowed). - Console: On form submission:
Response to preflight request doesn't pass access control check: It does not have HTTP ok status. - Network:
/filter:GET coursesworked with status200 OK.- Like button:
POST .../likeworked with status200 OK(simple request, no preflight needed). - Enrollment form:
OPTIONS enrollmentsreturned status400 Bad Requestwith bodyDisallowed CORS method. Response headers includedaccess-control-allow-origin: http://localhost:3000andaccess-control-allow-methods: GET.POST enrollmentswas not sent.
- Cause: Starlette's
CORSMiddlewaredefaults to allowing onlyGETwhenallow_methodsis omitted. The preflight check forPOSTwas rejected by the server with 400. - How I found it: Algorithm Step 4 (OPTIONS returned 400 Bad Request with body
Disallowed CORS methodandaccess-control-allow-methods: GET).
- What I did: Opened the application at
http://127.0.0.1:3000/filterwithallowedDevOrigins: ["127.0.0.1"]innext.config.ts. - Console:
Access to fetch at 'http://localhost:8000/courses' from origin 'http://127.0.0.1:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. - Network:
GET coursesrequest marked red with CORS error; backend terminal printedINFO: 127.0.0.1 - "GET /courses HTTP/1.1" 200 OK. - Cause: Browsers consider
localhostand127.0.0.1as entirely distinct origins. The backend whitelist only containedhttp://localhost:3000. - How I found it: Algorithm Step 3 (
Access-Control-Allow-Originwas missing for originhttp://127.0.0.1:3000).
- What I did: Introduced a typo in
main.pyinlist_courses: changedp["offset"]top["ofset"]. - Console:
Access to fetch at 'http://localhost:8000/courses' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. - Network:
/filter:GET coursesmarked red with CORS error./courses: Rendered Next.jserror.tsxboundary.- Like button and enrollment form: Worked normally because their routes were untouched.
- Backend Terminal: Status
500 Internal Server Errorwith traceback:ERROR: Exception in ASGI application Traceback (most recent call last): ... File ".../main.py", line 42, in list_courses return courses[p["ofset"] : p["offset"] + p["limit"]] KeyError: 'ofset' - Cause: An unhandled Python exception generated a 500 response from an outer ASGI error layer before
CORSMiddlewarecould attachAccess-Control-Allow-Originheaders. The browser masked the 500 error as a CORS error. - How I found it: Algorithm Step 2 and Step 5: Inspected the backend terminal to find
KeyError: 'ofset'and status500 Internal Server Error.
- Added
rewritesconfiguration tonext.config.ts:async rewrites() { return [ { source: "/api/:path*", destination: "http://localhost:8000/:path*", }, ]; }
- Requests made to
/api/coursesare proxied internally by the Next.js server to FastAPI, eliminating CORS from the browser's perspective.
- Generated
lib/schema.d.tsusingopenapi-typescript:pnpm exec openapi-typescript http://127.0.0.1:8000/openapi.json -o lib/schema.d.ts - Updated
lib/courses.tsto export:export type Course = components["schemas"]["Course"]; export type Enrollment = components["schemas"]["Enrollment"];
- In
course-catalog-api/main.py:cors_origins = [ origin.strip() for origin in os.getenv("CORS_ORIGINS", "http://localhost:3000").split(",") if origin.strip() ]
- Allows specifying multiple origins via
CORS_ORIGINS="http://localhost:3000,http://127.0.0.1:3000".
- When
cache: "no-store"is temporarily omitted fromgetCourses(), Next.js statically pre-renders/coursesat build time (marked○). Any changes to like counts or courses in the backend will not appear on/courseswithout rebuilding. - Dynamic route
/courses/[id]remainsƒ(server-rendered on demand) even withoutcache: "no-store"because it evaluates the dynamic segment parameterparams.id.
- Configured explicit lists:
app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_methods=["GET", "POST"], allow_headers=["Content-Type"], )
- Security Rationale: An explicit whitelist enforces the principle of least privilege, preventing malicious third-party origins from sending unauthorized HTTP methods (such as
DELETE,PUT,PATCH) or exfiltrating sensitive request headers (such asAuthorization).