Contributing to Movie Finder Frontend¶
This guide covers the conventions for contributors working on the Angular SPA.
For cross-cutting conventions (branching, commits, PRs, releases, submodule workflow) see the root CONTRIBUTING.md.
Table of contents¶
- Development setup
- Project structure
- Code standards
- Testing
- Adding a feature
- API integration
- Docker
- CI/CD
Development setup¶
Prerequisites¶
| Tool | Version | Install |
|---|---|---|
| Docker Desktop | Latest | docker.com |
| Make | 3.81+ | Standard on macOS/Linux |
First-time setup¶
cd frontend/
make init # pull image, npm ci, install git pre-commit hook
make editor-up # start dev container → http://localhost:4200
The dev server proxies /auth, /chat, and /health to http://localhost:8000. The backend must be running.
Start the backend (in a separate terminal):
Or use the full Docker stack from the root:
Project structure¶
frontend/
├── src/
│ ├── app/
│ │ ├── core/
│ │ │ ├── guards/
│ │ │ │ └── auth.guard.ts Redirects unauthenticated users to /login
│ │ │ ├── interceptors/
│ │ │ │ └── auth.interceptor.ts Injects JWT Bearer token on every outgoing request
│ │ │ ├── models.ts Shared TypeScript interfaces (ChatMessage, Session, etc.)
│ │ │ └── services/
│ │ │ ├── auth.service.ts register(), login(), refresh(), logout()
│ │ │ └── chat.service.ts SSE streaming, session management
│ │ ├── features/
│ │ │ ├── auth/
│ │ │ │ ├── login.component.ts /login page
│ │ │ │ └── register.component.ts /register page
│ │ │ └── chat/
│ │ │ ├── chat.component.ts Main chat page (/chat)
│ │ │ └── components/
│ │ │ ├── candidate-cards.component.ts Movie candidate cards (confirmation phase)
│ │ │ ├── movie-panel.component.ts Selected movie detail panel (qa phase)
│ │ │ ├── message-bubble.component.ts Individual chat message
│ │ │ └── session-sidebar.component.ts Session list sidebar
│ │ ├── app.ts Root component
│ │ ├── app.config.ts Application providers
│ │ └── app.routes.ts Route definitions
│ ├── environments/
│ │ ├── environment.ts Development (API at localhost:8000)
│ │ └── environment.prod.ts Production (reads API_URL from window.__env)
│ ├── index.html
│ ├── main.ts
│ └── styles.scss
├── proxy.conf.js Dev server proxy rules
├── Dockerfile Multi-stage: deps → builder → nginx runner
├── docker-entrypoint.sh Injects runtime env vars into nginx config
├── nginx.conf.template nginx config template
├── Jenkinsfile CI/CD pipeline
├── angular.json Angular CLI configuration
├── tsconfig.json TypeScript configuration
├── eslint.config.js ESLint flat config (v9)
└── .prettierrc Prettier configuration
Code standards¶
TypeScript¶
- Strict mode is enabled (
strict: trueintsconfig.json) - No
anytypes without a comment justifying the exception - Prefer
readonlyfor properties that do not need to be reassigned - Use Angular signals or
asyncpipe for reactive data — avoid manual subscriptions that outlive the component
Linting (ESLint 9 + angular-eslint)¶
Rules in eslint.config.js:
typescript-eslintrecommended + stylistic@angular-eslintcomponent and directive conventions@angular-eslint/templateHTML template rules
Formatting (Prettier 3)¶
Config in .prettierrc. Prettier and ESLint do not conflict — Prettier owns all formatting, ESLint owns correctness rules.
Type checking¶
Run this before opening a PR. CI will fail if there are type errors.
Angular conventions¶
| Convention | Rule |
|---|---|
| Component selectors | app- prefix, kebab-case |
| File names | <name>.component.ts, <name>.service.ts, <name>.guard.ts |
| Signals vs Observables | Prefer Angular signals for local state; RxJS Observables for SSE streaming |
| Template bindings | Use @if, @for control flow (Angular 17+ block syntax) |
| Component styles | SCSS, scoped to the component (no global styles except styles.scss) |
| Dependency injection | Use inject() function — not constructor injection |
Testing¶
Tests use Vitest with Angular TestBed utilities and @vitest/coverage-v8.
Coverage reports written to coverage/:
coverage/lcov.info— LCOV format (local tools, VS Code coverage extension)coverage/cobertura-coverage.xml— Cobertura XML (Jenkins Coverage plugin)
JUnit XML: set VITEST_JUNIT_OUTPUT_FILE=<path> to generate a JUnit report.
Testing rules¶
- Every service must have tests for its public methods
- Every component must have at minimum a "should create" test (smoke test)
- Mock HTTP calls in service tests — do not make real network requests
- Use
TestBedfor component tests requiring Angular's dependency injection - Coverage must not decrease on any PR
Example service test pattern¶
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
import { HttpClient } from '@angular/common/http';
describe('AuthService', () => {
let service: AuthService;
const mockHttp = { post: vi.fn() };
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AuthService, { provide: HttpClient, useValue: mockHttp }],
});
service = TestBed.inject(AuthService);
});
it('should create', () => {
expect(service).toBeTruthy();
});
});
Adding a feature¶
- Create a feature module under
src/app/features/<feature-name>/ - Add a route in
app.routes.ts - Guard the route with
authGuardif authentication is required: - Add a service in
core/services/if the feature requires backend communication - Write tests in
<name>.component.spec.tsalongside the component file - Update
models.tsif new shared interfaces are needed
Adding an API call¶
- Add the typed interface to
core/models.ts - Add the method to the relevant service in
core/services/ - The
AuthInterceptorautomatically attaches the Bearer token — no manual header setup needed - Handle 401 responses — the interceptor should trigger a token refresh or redirect to
/login
API integration¶
The frontend communicates with the FastAPI backend via:
| Mechanism | Used for |
|---|---|
| REST (JSON) | Auth endpoints (/auth/*), session management (/chat/sessions, /chat/{id}/history, DELETE /chat/{id}) |
| SSE (Server-Sent Events) | Chat streaming (POST /chat) |
SSE implementation note¶
Use fetch() with a ReadableStream decoder — not the browser's EventSource API. EventSource is GET-only and cannot carry a request body (required for POST /chat).
// Correct SSE pattern
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ session_id, message }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
// ... read chunks, parse `data: <json>` lines
SSE event shapes¶
// token event — arrives many times during streaming
{ type: 'token', content: string }
// done event — arrives exactly once, always last
{
type: 'done',
session_id: string,
reply: string,
phase: 'discovery' | 'confirmation' | 'qa',
candidates?: MovieCandidate[], // only when phase === 'confirmation'
confirmed_movie?: ConfirmedMovie // only when phase === 'qa'
}
The Q&A phase does not emit token events — the full reply arrives only in the done event.
Environment configuration¶
| File | Use |
|---|---|
environments/environment.ts |
Development — apiUrl: 'http://localhost:8000' |
environments/environment.prod.ts |
Production — reads window.__env.API_URL injected by docker-entrypoint.sh |
Docker¶
The Dockerfile uses three stages:
| Stage | Base | What happens |
|---|---|---|
deps |
node:20-alpine |
npm ci (mount cache) |
builder |
node:20-alpine |
ng build --configuration=production |
runner |
nginx:stable-alpine |
Serve the Angular bundle; proxy /api to backend |
Runtime environment variables injected at container start by docker-entrypoint.sh:
| Variable | Default | Purpose |
|---|---|---|
API_URL |
"" (empty) |
Passed to window.__env.API_URL in the Angular app |
BACKEND_URL |
http://backend:8000 |
nginx upstream target for /api proxy |
Build locally:
docker build -t movie-finder-frontend:local .
docker run -p 80:80 \
-e API_URL="" \
-e BACKEND_URL="http://host.docker.internal:8000" \
movie-finder-frontend:local
CI/CD¶
The Jenkinsfile runs three modes based on Git context:
| Mode | Trigger | Stages |
|---|---|---|
| CONTRIBUTION | Feature branch / PR | Type-check → Lint → Test |
| INTEGRATION | Push to main |
+ Build Docker image → Push :sha8 + :latest to ACR → (opt) Staging deploy |
| RELEASE | v* tag |
+ Push :v1.2.3 to ACR → (opt) Production deploy |
CONTRIBUTION builds must be green before any PR can be merged. No images are built or pushed in CONTRIBUTION mode.
Jenkins credentials required (frontend pipeline):
| ID | Kind | Purpose |
|---|---|---|
acr-login-server |
Secret text | ACR hostname |
acr-credentials |
Username+Password | docker login to ACR |
azure-sp |
Username+Password | az login for Container App updates |
See docs/devops/setup.md §9 for setup instructions.