A stateless RESTful API for browsing movies, directors, and genres, with user registration, JWT-based authentication, and favourite-movie management.
Built with Node.js, Express, MongoDB, and Passport.js as part of a full-stack project made in MERN stack (MongoDB, Express, React, Node.js).
- Backend API: Render deployment
- Frontend client: React (Netlify)
- 1. Project structure and design
- 2. Database
- 3. Authentication and Authorization
- 4. Middleware Stack
- 5. API Endpoints
- 6. Environment Variables
- 7. Local Setup
- 8. Future Improvements
The codebase follows a layered monolithic architecture, with each layer having a single responsibility, making the codebase easier to review, test and maintain.
| Layer | Directory | Responsibility |
|---|---|---|
| Routes | routes/ |
Manage the various endpoints of the API so that each route file is a single resource (e.g., movies.js, users.js). |
| Controllers | controllers/ |
Own the request/response lifecycle: parse input, delegate to models and format HTTP responses. |
| Models | models/ |
Schema definitions made with Mongoose, of which fields include include data types, requiremente and references. |
| Middleware | middleware/ |
Connects different layers of the application to handle shared concerns and manage pipeline flow (e.g., logging, error handling, authentication). |
| Application Factory | app.js |
Assembles the Express application, mounts middleware and routes together. |
| Entry Point | index.js |
Loads environment variables, connects to MongoDB and starts the HTTP server. |
This project uses MongoDB, a NoSQL document database that stores data as JSON-like documents rather than table rows.
The database has five collections: movies, users, genres, directors, and actors.
-
MongoDB is however schema-less, meaning that documents in the same collection can have different fields. This flexibility is useful for rapid development, but it also means that malformed data can creep into the database if not validated at the application layer.
Therefore, the project uses Mongoose, an ODM (Object-Document Mapper) to add a schema layer on top of the database and enforce data integrity. With defined schemas, Mongoose validates incoming data before it is saved to the database, ensuring that malformed data never reaches the database. -
Each collection has its own file in
models/with a Mongoose schema:// models/Movie.js const movieSchema = mongoose.Schema({ title: { type: String, required: true }, description: { type: String, required: true }, genre: { type: mongoose.Schema.Types.ObjectId, ref: "Genre", required: true }, director: { type: mongoose.Schema.Types.ObjectId, ref: "Director", required: true }, actors: [{ type: mongoose.Schema.Types.ObjectId, ref: "Actor" }], imagePath: String, });
Related entities are stored in separate collections and linked via
ObjectIdreferences (for example, aMoviestores itsgenreanddirectoras IDs rather than embedding the full documents). This avoids data duplication while still sending a fully resolved object in one response avoiding redundant queries.
The API is fully stateless: it does not use sessions, cookies, or CSRF tokens. Instead, it uses JWTs (JSON Web Tokens) for authentication and authorization. This means that the server never stores any record of who is logged in: every request must carry a self-contained token that the server can verify on its own, so any instance of the API can handle any request without consulting a shared session store.
- LocalStrategy: handles initial login request. It extracts
usernameandpasswordfrom the request body, looks up the user and uses bcrypt to verify the password. On success, a JWT is generated and returned to the client. - JWTStrategy: guards every protected route. It pulls the token from the
Authorization: Bearer <token>header to verify its signature, and resolves the full user document. If the token is expired, tampered with or missing, Passport returns401 Unauthorizedand the route handler never executes.
Secure password storage and verification is handled by bcrypt, a one-way hashing algorithm used so that the server never stores plaintext passwords and make it hard for attackers to recover them even if the database is compromised.
The middleware pipeline in app.js executes in this order on every request:
| Middleware | Purpose |
|---|---|
morgan("common") |
Structured request logging to stdout (method, URL, status, response time) |
bodyParser.json() |
Parses Content-Type: application/json bodies into req.body |
bodyParser.urlencoded() |
Parses form-encoded payloads |
cors.js |
Whitelists allowed origins (localhost:1234, mymovielistj.netlify.app); rejects all others with a descriptive error |
passport.initialize() |
Bootstraps Passport's authentication machinery |
passport.js (required) |
Registers Local + JWT strategies with Passport |
errorHandler.js |
Catch-all Express error middleware (4-arity function). Catches unhandled errors from any prior middleware and returns 500 |
Route-level middleware (passport.authenticate("jwt", { session: false })) is applied per-route in the route files, not globally - public routes like POST /users (signup) and POST /login bypass it.
All endpoints return JSON. Protected endpoints require Authorization: Bearer <token>.
| Method | Endpoint | Description |
|---|---|---|
POST |
/users |
Register a new user. Body validated by express-validator. Password bcrypt-hashed before storage. |
POST |
/login |
Authenticate with username + password. Returns { user, token }. |
| Method | Endpoint | Description |
|---|---|---|
GET |
/movies |
List all movies with populated genre & director |
GET |
/movies/:title |
Single movie by title (exact match) |
GET |
/genres |
List all genres |
GET |
/genres/:name |
Single genre with associated movies |
GET |
/directors |
List all directors |
GET |
/directors/:name |
Single director with associated movies |
GET |
/users |
List all users |
GET |
/users/:username |
Single user profile |
PUT |
/users/:username |
Update user info. Re-validated, password re-hashed. |
POST |
/users/:username/movies/:MovieID |
Add movie to user's favourites |
DELETE |
/users/:username/movies/:MovieID |
Remove movie from user's favourites |
DELETE |
/users/:username |
Delete user account |
Note on status codes: Successful
GETandPUToperations return200/201. Validation failures return422. Auth failures return401. Not-found returns404. Server errors return500via the catch-all error handler.
Local .env file is never committed but a template is provided as example.env as reference:
| Variable | Required | Purpose |
|---|---|---|
DB_URI |
Yes | MongoDB connection string (Atlas cluster or local instance) |
JWT_SECRET |
Yes | HMAC-SHA256 signing key for JWT tokens. Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" |
PORT |
No | Server listen port. Defaults to 8080. Set automatically by Render in production. |
An .env file is needed for local development while on Render it is not used and any env vars can be injected through the platform's dashboard Environment → Environment Variables
-
- Node.js ≥ 21 (required for native
--env-fileflag) - MongoDB running locally on
127.0.0.1:27017, or a MongoDB Atlas cluster
- Node.js ≥ 21 (required for native
-
# 1. Clone the repository git clone <repo-url> cd movie_api # 2. Install dependencies npm install # 3. Create your .env file from the template cp example.env .env # Edit .env with your actual DB_URI and JWT_SECRET # 4. Start in development mode (auto-restart on changes) npm run dev # 5. Or start in production mode npm start
To run the app locally use:
npm run dev
This command uses
nodemon --env-file=.env, which loads the.envfile intoprocess.envvia Node's native env-file support. Make sure to create a.envfile in the root directory with your MongoDB connection string and JWT secret before running.Note: npm start is used for production deployment on Render, this command does not use the
.envfile since Render injects environment variables at the OS level.
- Stronger input validation: improve
express-validatorwith better rules to handle login and signup edge cases. - Structured logging: replace Morgan with Pino for better detailed logging.
- Rate limiting & security hardening: add express-rate-limit, helmet, and CORS tightening to protect against brute-force and common web vulnerabilities.
- Testing: add unit tests and integration tests.
- Admin role: introduce an admin role to keep user data private and allow for database management via an admin dashboard.
- Main Concept: redesign the app concept from a generic movie database into a more personalizable, user-driven experience.