A modern platform for discovering, reviewing, and sharing movies with real-time Finnish theater data. Built with React, Node.js, and PostgreSQL as part of a project at Oulu University of Applied Sciences (OAMK).
Movie App brings together social interaction and movie discovery in one responsive web experience. Users can create watch groups, share favorite lists, and exchange reviews - all in real time. The app is fully bilingual (English/Finnish), optimized for all devices, and integrates external APIs to deliver live theater showtimes and up-to-date movie data.
- Introduction
- Features
- Technology Stack
- Installation & Setup
- Directory Structure
- Database Architecture
- API endpoints
- Auth: JWT login/sign-up, password change, account deletion.
- Search: Filters (genre, rating, year) + sorting; daily popular pick on Home.
- Reviews: Write/edit reviews; public "Latest reviews" feed.
- Groups: Create, join (request/approve), moderate; shared movie list + showtimes.
- Favorites: Add/remove; share as public link (slug).
- Realtime: SSE for group changes and shared lists.
- Theaters (FI): Finnkino shows by theater/date; metro highlights on Home.
- i18n: English and Finnish via i18next.
- Responsive: Desktop, tablet, mobile.
Frontend: React 19, Vite 7, Tailwind CSS 4, React Router 7, i18next, React Toastify
Backend: Node.js + Express 5 (SSE for live updates)
Database: PostgreSQL 14+ (pg)
Auth: JWT + bcrypt password hashing
APIs: TMDB (v3 endpoints, V4 Bearer auth), Finnkino XML API
- Node.js version 18 or higher
- PostgreSQL version 14 or higher
git clone https://github.com/andytrix/MovieApp-FullStack.git
cd MovieApp-FullStackCreate a new local PostgreSQL database (you can name it freely, e.g., movieapp):
createdb movieappLoad schema from the SQL file:
psql -U postgres -d movieapp -f backend/movieApp.sqlNavigate to the backend folder and install dependencies:
cd backend
npm installCreate a .env file inside the backend folder:
# backend/.env
PORT=3001
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=your_password
DB_NAME=movieapp
# Optional test database (used when NODE_ENV=test)
TEST_DB_NAME=movieapp_test
# JWT secret
JWT_SECRET=your_long_random_secret
# TMDB V4 Bearer token (required for TMDB requests)
# Get from https://developer.themoviedb.org/docs/authentication
TMDB_V4_TOKEN=your_tmdb_v4_bearer_tokenStart the backend in development mode:
npm run devStartThe backend API should now be running at http://localhost:3001.
Open a new terminal window at the project root:
npm installCreate a .env file:
# .env
VITE_API_URL=http://localhost:3001
# No TMDB key needed in frontend (backend proxies TMDB)Start the frontend:
npm run devVite will display a local development URL, for example: http://localhost:5173.
- Protected API routes require a valid JWT token in the
Authorizationheader. - The frontend stores auth (user + token) in localStorage and attaches the token to API calls.
If you need a separate database for automated tests, create a new one (e.g., movieapp_test), set TEST_DB_NAME in backend/.env, and start the backend in test mode:
NODE_ENV=test npm run devStartYou can also run backend tests (Mocha/Chai) from backend/:
cd backend
npm testKeep the backend running in test mode (NODE_ENV=test) while tests execute.
- CORS or Mixed Content Errors: Ensure
VITE_API_URLin the frontend.envmatches your backend address. - Database connection failed: Double-check PostgreSQL is running and credentials in
backend/.envare correct. - TMDB errors (401/403): Make sure
TMDB_V4_TOKENis set inbackend/.envand valid. - Frontend not updating: Restart both backend and frontend after any
.envor schema changes.
After completing these steps, the full-stack application should be accessible locally with React (frontend) and Node.js (backend) connected to a PostgreSQL database.
Open two terminals and run:
# Terminal 1 – backend
cd backend
npm install
npm run devStart
# Terminal 2 – frontend (from project root)
npm install
npm run dev- Frontend: http://localhost:5173 (by default)
- Backend API: http://localhost:3001
- backend/.env
- PORT=3001
- DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME
- TEST_DB_NAME (optional, tests; used when NODE_ENV=test)
- JWT_SECRET (required)
- TMDB_V4_TOKEN (required; TMDB Bearer V4)
- TMDB_BASE_URL (optional; defaults to https://api.themoviedb.org/3)
- .env (frontend)
- VITE_API_URL=http://localhost:3001
MovieApp-FullStack/
├─ README.md # Project overview & setup
├─ package.json # Frontend deps & scripts
├─ vite.config.js # Vite config
├─ tailwind.config.js # Tailwind config
├─ eslint.config.js # ESLint rules
├─ index.html # Vite index template
├─ .env # Frontend env (VITE_API_URL)
├─ backend/ # Express API & DB layer
│ ├─ package.json # Backend deps (Express, pg)
│ ├─ index.js # Express app & routers
│ ├─ movieApp.sql # PostgreSQL schema
│ ├─ .env # Backend env (DB, JWT, TMDB)
│ ├─ controllers/ # Route handlers
│ │ ├─ userController.js # Auth, profile, password, delete
│ │ ├─ searchController.js # TMDB search & genres
│ │ ├─ movieController.js # Movie details, popular today
│ │ ├─ reviewController.js # Reviews CRUD & latest
│ │ ├─ favoritesController.js # Favorites + share (SSE)
│ │ └─ groupController.js # Groups, members, movies, shows (SSE)
│ ├─ routes/ # Express routers
│ │ ├─ userRouter.js # /api/user
│ │ ├─ searchRouter.js # /api/search
│ │ ├─ movieRouter.js # /api/movies (+ /:id/reviews)
│ │ ├─ reviewRouter.js # /api/movies/:id/reviews (auth)
│ │ ├─ reviewsPublicRouter.js # /api/reviews/latest (public)
│ │ ├─ favoritesRouter.js # /api/favorites (+ /share, /stream)
│ │ └─ groupRouter.js # /api/groups (+ /stream)
│ ├─ models/ # SQL helpers (pg)
│ │ ├─ userModel.js # Users (bcrypt)
│ │ ├─ reviewModel.js # Reviews CRUD & latest
│ │ ├─ favoritesModel.js # Favorites + sharing
│ │ └─ groupModel.js # Groups, members, movies, shows
│ ├─ helper/ # Utilities & clients
│ │ ├─ db.js # pg Pool, env-based DB
│ │ ├─ auth.js # JWT middleware
│ │ ├─ tmdbClient.js # TMDB client (V4)
│ │ └─ test.js # Test helpers
│ └─ test/ # Mocha/Chai API tests
│ ├─ setup.test.js # Test setup (DB, server)
│ ├─ auth.*.test.js # Auth flows
│ ├─ user.deleteAccount.test.js # Delete account
│ └─ reviews.browse.test.js # Public latest reviews
├─ src/ # React frontend (Vite)
│ ├─ main.jsx # App entrypoint (i18n init)
│ ├─ App.jsx # Routes & layout
│ ├─ i18n.js # i18next config (EN/FI)
│ ├─ assets/ # Static assets (images)
│ ├─ components/ # Reusable UI
│ │ ├─ Navbar.jsx # Top navigation
│ │ ├─ Footer.jsx # Footer links
│ │ ├─ FavoriteButton.jsx # Add/remove favorites
│ │ ├─ MovieReviews.jsx # Movie reviews (CRUD)
│ │ ├─ ProtectedRoute.jsx # Auth-guard
│ │ └─ FancySelect.jsx # Styled select
│ ├─ context/ # Auth/user context
│ │ ├─ UserProvider.jsx # Auth state & favorites
│ │ ├─ UserContext.js # React context
│ │ └─ useUser.js # User hook
│ ├─ layouts/ # Page layouts
│ │ └─ AppLayout.jsx # App shell
│ ├─ lib/ # API clients & helpers
│ │ ├─ api.js # Fetch wrapper & auth
│ │ └─ api/ # Domain clients
│ │ ├─ movies.js # Movie API (TMDB)
│ │ ├─ search.js # Search endpoints
│ │ ├─ reviews.js # Reviews endpoints
│ │ └─ shows.js # Finnkino (XML)
│ ├─ pages/ # Route pages
│ │ ├─ Home.jsx # Daily pick, shows, latest reviews
│ │ ├─ Search.jsx # Movie search (filters/sort)
│ │ ├─ MovieDetails.jsx # Details, cast, recommendations
│ │ ├─ Reviews.jsx # Public latest reviews
│ │ ├─ Favorites.jsx # My favorites & shared (SSE)
│ │ ├─ SharedFavorites*.jsx # Public shared favorites
│ │ ├─ Groups.jsx # Group list (SSE)
│ │ ├─ GroupPage.jsx # Group details, members, shows (SSE)
│ │ ├─ Theaters.jsx # Finnkino theater/day
│ │ ├─ Authentication.jsx # Sign up
│ │ ├─ Login.jsx # Sign in
│ │ ├─ Account.jsx # Profile & delete
│ │ ├─ ChangePassword.jsx # Change password
│ │ └─ NotFound.jsx # 404
│ └─ styles/ # Tailwind & custom styles
│ └─ tailwind.css # Tailwind layers
└─ documents/ # Docs & assets
Key entities (table names):
- "user" — accounts (id UUID, email, password_hash, created_at)
- "review" — movie reviews (id, movie_id BIGINT, user_id UUID, rating 1–5, text)
- favorites — user favorite movies (id, user_id UUID, movie_id INT)
- favorite_share — public share settings (user_id PK, display_name, slug UUID, is_shared)
- "group" — watch groups (id BIGSERIAL, name, owner_id UUID)
- group_membership — membership with status/role (id, group_id, user_id, status, role)
- group_movie — movies in a group (id, group_id, movie_id BIGINT, title, added_by)
- group_showtime — scheduled times (id, group_movie_id, starts_at, theater, auditorium)
-
Auth / User
- POST /api/user/signup — create account (email, password)
- POST /api/user/signin — login, returns JWT
- PATCH /api/user/me/password — change password (auth)
- DELETE /api/user/me — delete my account (auth)
- GET /api/user/profile — current user profile (auth)
-
Search / Movies (TMDB-backed)
- GET /api/search/genres — movie genres (?language)
- GET /api/search/movies — search (?q, page, language, minRating, genre, yearFrom, yearTo, sort)
- GET /api/movies/popular/today — daily popular pick
- GET /api/movies/:id — movie details (with extras)
-
Reviews
- GET /api/reviews/latest — public latest reviews (page, limit)
- GET /api/movies/:movieId/reviews — list reviews for movie (public)
- POST /api/movies/:movieId/reviews — upsert my review (auth)
- PATCH /api/movies/:movieId/reviews/:id — edit my review (auth)
- DELETE /api/movies/:movieId/reviews/:id — delete my review (auth)
-
Favorites & Sharing
- GET /api/favorites/:userId — my favorite movie ids
- POST /api/favorites — add favorite ({ userId, movieId })
- DELETE /api/favorites/:userId/:movieId — remove favorite
- GET /api/favorites/shared — list public shares (public)
- GET /api/favorites/share/me — my share status (auth)
- POST /api/favorites/share — upsert share { is_shared, display_name } (auth)
- GET /api/favorites/share/:slug — share metadata (public)
- GET /api/favorites/share/:slug/movies — share movie ids (public)
- GET /api/favorites/stream — SSE for share list changes (public)
-
Groups
- GET /api/groups — list groups (public)
- POST /api/groups — create group (auth)
- GET /api/groups/stream — SSE for group list (auth via ?token)
- GET /api/groups/:id — group details (public)
- POST /api/groups/:id/join — request to join (auth)
- GET /api/groups/:id/members — members (auth; if owner/approved)
- PATCH /api/groups/:id/members/:userId — approve/reject (auth; owner/mod)
- DELETE /api/groups/:id/members/me — leave (auth)
- DELETE /api/groups/:id — delete group (auth; owner)
- DELETE /api/groups/:id/members/:userId — remove member (auth; owner/mod)
- GET /api/groups/:id/movies — list movies (auth; members/owner)
- POST /api/groups/:id/movies — add movie (auth; members)
- DELETE /api/groups/:id/movies/:gmId — remove movie (auth; added-by)
- POST /api/groups/:id/movies/:gmId/showtimes — add showtime (auth; members)
- DELETE /api/groups/:id/movies/:gmId/showtimes/:sid — remove showtime (auth; added-by)
- GET /api/groups/:id/stream — SSE for group (auth via ?token)
- GET /api/groups/:id/membership/me — my membership (auth)

