Comprehensive REST API for Movie Streaming Platform with User Management, Movie CRUD, Episodes, and Admin Controls.
- Overview
- Tech Stack
- Setup & Installation
- API Endpoints
- Authentication
- Error Handling
- Request/Response Examples
- Testing
A full-featured movie streaming backend API that provides:
- ✅ User authentication with JWT tokens
- ✅ Complete Movie CRUD operations
- ✅ Episode management for movies
- ✅ User profile management
- ✅ Admin controls for user and movie management
- ✅ Role-based access control (RBAC)
| Component | Technology |
|---|---|
| Language | Java 21 |
| Framework | Spring Boot 3.3.0 |
| Database | MySQL 8.0 |
| ORM | JPA/Hibernate |
| Security | Spring Security + JWT |
| Build Tool | Maven |
| API Style | RESTful |
- Java 21+ installed
- MySQL 8.0+ running
- Maven 3.8+
- Git
The application uses Spring Boot properties for configuration. Two files are provided:
- Base configuration with non-sensitive defaults
- Database URL, API prefix, email host/port, etc.
- File:
src/main/resources/application.properties
- Local development credentials (passwords, API keys, JWT secrets)
- Override sensitive values for local testing
- File:
src/main/resources/application-local.properties - Add to
.gitignore(already done)
Configuration Example:
# application-local.properties (local secrets only)
spring.datasource.password=YOUR_DB_PASSWORD
jwt.secret-key=YOUR_JWT_SECRET
spring.mail.username=YOUR_MAILTRAP_EMAIL
spring.mail.password=YOUR_MAILTRAP_PASSWORDAll base configuration is in application.properties - only override sensitive values in application-local.properties.
Option 1: Using VS Code (Recommended)
- Press
F5to start with local profile - Configured in
.vscode/launch.json
Option 2: Using Maven
# Clone project
cd movie-streaming-api
# Build
mvn clean compile
# Run with local profile
mvn spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=local"
# OR debug mode
java -agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=localhost:5005 \
-Dspring.profiles.active=local \
-jar target/movie-streaming-api-0.0.1-SNAPSHOT.jarPrerequisites:
- MySQL running (via Docker):
docker-compose up mysql application-local.propertiesconfigured with credentials
Default URL: http://localhost:8080
All endpoints use JSON request/response.
Endpoint:
POST /api/v1/auth/register
Description: Create new user account
Request:
{
"username": "testuser",
"email": "test@example.com",
"password": "password123",
"fullName": "Test User"
}Response: (200 OK)
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"userId": 1,
"username": "testuser",
"email": "test@example.com",
"message": "Đăng ký thành công"
}Error Responses:
400- Username/email already exists400- Invalid input validation
Endpoint:
POST /api/v1/auth/login
Description: Authenticate user and get tokens
Request:
{
"usernameOrEmail": "testuser",
"password": "password123"
}Response: (200 OK)
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"userId": 1,
"username": "testuser",
"email": "test@example.com"
}Error Responses:
401- Invalid credentials404- User not found
Endpoint:
GET /api/v1/auth/me
Description: Get authenticated user info
Headers:
Authorization: Bearer {{accessToken}}
Response: (200 OK)
{
"id": 1,
"username": "testuser",
"email": "test@example.com",
"fullName": "Test User",
"role": "ROLE_USER"
}Error Responses:
401- Unauthorized (missing/invalid token)
Endpoint:
POST /api/v1/auth/refresh
Description: Get new access token using refresh token
Request:
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}Response: (200 OK)
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 86400000
}Error Responses:
401- Invalid/expired refresh token
Endpoint:
POST /api/v1/auth/logout
Description: Invalidate refresh token
Headers:
Authorization: Bearer {{accessToken}}
Request:
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}Response: (200 OK)
{
"message": "Đăng xuất thành công"
}Endpoint:
POST /api/v1/auth/forgot-password
Description: Request password reset token
Request:
{
"email": "test@example.com"
}Response: (200 OK)
{
"message": "Reset token: abc123xyz789...",
"resetToken": "abc123xyz789..."
}Error Responses:
404- User not found
Endpoint:
POST /api/v1/auth/reset-password
Description: Reset password with token
Request:
{
"token": "abc123xyz789...",
"newPassword": "newpassword456"
}Response: (200 OK)
{
"message": "Đặt lại mật khẩu thành công"
}Error Responses:
400- Invalid/expired token404- User not found
Public endpoints - No authentication required.
Endpoint:
GET /api/v1/movies
Description: Get all published movies
Response: (200 OK)
[
{
"id": 1,
"title": "Avengers",
"slug": "avengers",
"posterUrl": "https://example.com/poster.jpg",
"releaseYear": 2019,
"country": "USA",
"language": "English",
"ageRating": "PG-13",
"movieType": "SINGLE",
"isPremiumOnly": false,
"averageRating": 8.5,
"viewCount": 1000
}
]Endpoint:
GET /api/v1/movies/{id}
Parameters:
id(path, required): Movie ID (must be positive)
Response: (200 OK)
{
"id": 1,
"title": "Avengers",
"originalTitle": "Avengers: Endgame",
"slug": "avengers",
"description": "Thanos threatens the entire universe...",
"posterUrl": "https://example.com/poster.jpg",
"bannerUrl": "https://example.com/banner.jpg",
"trailerUrl": "https://example.com/trailer.mp4",
"releaseYear": 2019,
"country": "USA",
"language": "English",
"ageRating": "PG-13",
"movieStatus": "PUBLISHED",
"movieType": "SINGLE",
"isPremiumOnly": false,
"viewCount": 1000,
"favoriteCount": 500,
"averageRating": 8.5,
"totalRatings": 250,
"totalReviews": 150,
"publishedAt": "2019-04-26T00:00:00",
"episodes": [
{
"id": 1,
"title": "Episode 1",
"episodeNumber": 1,
"videoUrl": "https://example.com/video1.mp4",
"thumbnailUrl": "https://example.com/thumb1.jpg",
"durationSeconds": 3600,
"isFreePreview": true,
"status": "PUBLISHED"
}
]
}Error Responses:
404- Movie not found400- Invalid ID (negative/non-numeric)
Endpoint:
GET /api/v1/movies/slug/{slug}
Parameters:
slug(path, required): Movie slug (URL-friendly name)
Response: (200 OK) Same as Get Movie by ID response
Error Responses:
404- Movie not found
Endpoint:
GET /api/v1/movies/{id}/episodes
Parameters:
id(path, required): Movie ID
Response: (200 OK)
[
{
"id": 1,
"title": "Episode 1",
"episodeNumber": 1,
"videoUrl": "https://example.com/video1.mp4",
"thumbnailUrl": "https://example.com/thumb1.jpg",
"durationSeconds": 3600,
"isFreePreview": true,
"status": "PUBLISHED"
},
{
"id": 2,
"title": "Episode 2",
"episodeNumber": 2,
"videoUrl": "https://example.com/video2.mp4",
"thumbnailUrl": "https://example.com/thumb2.jpg",
"durationSeconds": 3600,
"isFreePreview": false,
"status": "PUBLISHED"
}
]Admin-only endpoints. Requires ROLE_ADMIN JWT token.
Auth Headers (All Admin Endpoints):
Authorization: Bearer {{adminAccessToken}}
Content-Type: application/json
Endpoint:
POST /api/v1/admin/movies
Request:
{
"title": "New Movie",
"originalTitle": "Original Title",
"slug": "new-movie-unique",
"description": "Movie description...",
"posterUrl": "https://example.com/poster.jpg",
"bannerUrl": "https://example.com/banner.jpg",
"trailerUrl": "https://example.com/trailer.mp4",
"releaseYear": 2024,
"country": "USA",
"language": "English",
"ageRating": "PG-13",
"movieType": "SINGLE",
"movieStatus": "DRAFT",
"isPremiumOnly": false
}Response: (200 OK)
{
"id": 10,
"title": "New Movie",
"slug": "new-movie-unique",
"description": "Movie description...",
"movieType": "SINGLE",
"movieStatus": "DRAFT",
"isPremiumOnly": false,
"createdAt": "2024-04-06T21:00:00"
}Error Responses:
409- Slug already exists403- Forbidden (not admin)400- Validation error
Endpoint:
PUT /api/v1/admin/movies/{id}
Parameters:
id(path, required): Movie ID
Request:
{
"title": "Updated Title",
"description": "Updated description...",
"posterUrl": "https://example.com/new-poster.jpg",
"releaseYear": 2024,
"movieType": "SERIES",
"movieStatus": "PUBLISHED",
"isPremiumOnly": true
}Response: (200 OK)
{
"id": 10,
"title": "Updated Title",
"movieStatus": "PUBLISHED",
"movieType": "SERIES",
"isPremiumOnly": true,
"publishedAt": "2024-04-06T21:05:00",
"updatedAt": "2024-04-06T21:05:00"
}Error Responses:
404- Movie not found403- Forbidden400- Validation error
Endpoint:
DELETE /api/v1/admin/movies/{id}
Parameters:
id(path, required): Movie ID
Response: (204 No Content)
Error Responses:
404- Movie not found403- Forbidden
Endpoint:
POST /api/v1/admin/movies/{id}/episodes
Parameters:
id(path, required): Movie ID
Request:
{
"title": "Episode 1",
"episodeNumber": 1,
"videoUrl": "https://example.com/video1.mp4",
"thumbnailUrl": "https://example.com/thumb1.jpg",
"durationSeconds": 3600,
"isFreePreview": true,
"status": "PUBLISHED"
}Response: (200 OK)
{
"id": 1,
"title": "Episode 1",
"episodeNumber": 1,
"videoUrl": "https://example.com/video1.mp4",
"thumbnailUrl": "https://example.com/thumb1.jpg",
"durationSeconds": 3600,
"isFreePreview": true,
"status": "PUBLISHED",
"createdAt": "2024-04-06T21:10:00"
}Error Responses:
404- Movie not found403- Forbidden400- Validation error
Endpoint:
DELETE /api/v1/admin/movies/{id}/episodes/{episodeId}
Parameters:
id(path, required): Movie IDepisodeId(path, required): Episode ID
Response: (204 No Content)
Error Responses:
404- Movie/Episode not found403- Forbidden
Auth Headers:
Authorization: Bearer {{accessToken}}
Content-Type: application/json
Endpoint:
GET /api/v1/users/me
Response: (200 OK)
{
"id": 1,
"username": "testuser",
"email": "test@example.com",
"fullName": "Test User",
"avatarUrl": "https://example.com/avatar.jpg",
"role": "ROLE_USER",
"accountStatus": "ACTIVE",
"createdAt": "2024-01-01T00:00:00"
}Error Responses:
401- Unauthorized
Endpoint:
PUT /api/v1/users/me
Request:
{
"fullName": "Updated Name",
"email": "newemail@example.com",
"avatarUrl": "https://example.com/new-avatar.jpg"
}Response: (200 OK)
{
"id": 1,
"username": "testuser",
"email": "newemail@example.com",
"fullName": "Updated Name",
"avatarUrl": "https://example.com/new-avatar.jpg",
"updatedAt": "2024-04-06T21:15:00"
}Error Responses:
401- Unauthorized400- Validation error
Endpoint:
PATCH /api/v1/users/me/password
Request:
{
"oldPassword": "password123",
"newPassword": "newpassword456"
}Response: (200 OK)
{
"message": "Đổi mật khẩu thành công"
}Error Responses:
401- Unauthorized400- Invalid old password
Auth Headers (All Admin Endpoints):
Authorization: Bearer {{adminAccessToken}}
Content-Type: application/json
Endpoint:
GET /api/v1/users
Query Parameters:
page(optional): Page number (default: 0)size(optional): Page size (default: 20)
Response: (200 OK)
[
{
"id": 1,
"username": "testuser",
"email": "test@example.com",
"fullName": "Test User",
"role": "ROLE_USER",
"accountStatus": "ACTIVE"
},
{
"id": 2,
"username": "admin",
"email": "admin@example.com",
"fullName": "Admin User",
"role": "ROLE_ADMIN",
"accountStatus": "ACTIVE"
}
]Error Responses:
403- Forbidden (not admin)
Endpoint:
GET /api/v1/users/{id}
Parameters:
id(path, required): User ID
Response: (200 OK)
{
"id": 1,
"username": "testuser",
"email": "test@example.com",
"fullName": "Test User",
"avatarUrl": "https://example.com/avatar.jpg",
"role": "ROLE_USER",
"accountStatus": "ACTIVE",
"premiumExpiryDate": null,
"createdAt": "2024-01-01T00:00:00",
"lastLoginAt": "2024-04-06T20:00:00"
}Error Responses:
404- User not found403- Forbidden
Endpoint:
PATCH /api/v1/users/{id}/status
Parameters:
id(path, required): User ID
Request:
{
"status": "ACTIVE"
}Valid Status Values:
ACTIVE- User can loginBLOCKED- User cannot loginDELETED- User account deleted
Response: (200 OK)
{
"id": 1,
"username": "testuser",
"accountStatus": "BLOCKED",
"updatedAt": "2024-04-06T21:20:00"
}Error Responses:
404- User not found403- Forbidden400- Invalid status
Endpoint:
PATCH /api/v1/users/{id}/role
Parameters:
id(path, required): User ID
Request:
{
"role": "ROLE_ADMIN"
}Valid Role Values:
ROLE_ADMIN- AdministratorROLE_USER- Regular user
Response: (200 OK)
{
"id": 1,
"username": "testuser",
"role": "ROLE_ADMIN",
"updatedAt": "2024-04-06T21:25:00"
}Error Responses:
404- User not found403- Forbidden400- Invalid role
Endpoint:
DELETE /api/v1/users/{id}
Parameters:
id(path, required): User ID
Response: (204 No Content)
Error Responses:
404- User not found403- Forbidden
Access Token: Short-lived token (24 hours) used for API requests Refresh Token: Long-lived token used to get new access tokens
All protected endpoints require Bearer token:
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
http://localhost:8080/api/v1/users/me- Access token expires
- Use Refresh Token endpoint to get new token pair
- Use new Access Token for subsequent requests
{
"timestamp": "2024-04-06T21:30:00",
"status": 400,
"error": "Bad Request",
"message": "Validation failed",
"path": "/api/v1/movies"
}| Status | Meaning | Common Cause |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created |
| 204 | No Content | DELETE success |
| 400 | Bad Request | Invalid input |
| 401 | Unauthorized | Missing/invalid token |
| 403 | Forbidden | No permission (not admin) |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate (slug exists) |
| 500 | Server Error | Server error |
| Code | Message | Solution |
|---|---|---|
USERNAME_ALREADY_EXISTS |
Username taken | Choose different username |
EMAIL_ALREADY_EXISTS |
Email taken | Choose different email |
MOVIE_NOT_FOUND |
Movie doesn't exist | Check movie ID |
USER_NOT_FOUND |
User doesn't exist | Check user ID |
MOVIE_SLUG_EXISTED |
Slug already used | Change movie slug |
INVALID_CREDENTIALS |
Wrong password | Check credentials |
INVALID_TOKEN |
Token expired/invalid | Refresh token for new one |
curl -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"username": "newuser",
"email": "newuser@example.com",
"password": "password123",
"fullName": "New User"
}'Response:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"userId": 3,
"username": "newuser",
"email": "newuser@example.com"
}export ACCESS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
export REFRESH_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."curl http://localhost:8080/api/v1/users/me \
-H "Authorization: Bearer $ACCESS_TOKEN"curl http://localhost:8080/api/v1/moviescurl -X POST http://localhost:8080/api/v1/admin/movies \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Inception",
"slug": "inception-2024",
"description": "A mind-bending thriller",
"releaseYear": 2024,
"movieType": "SINGLE",
"movieStatus": "DRAFT",
"isPremiumOnly": false
}'curl -X POST http://localhost:8080/api/v1/admin/movies/1/episodes \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Full Movie",
"episodeNumber": 1,
"videoUrl": "https://example.com/inception.mp4",
"durationSeconds": 8820,
"isFreePreview": false,
"status": "PUBLISHED"
}'curl -X PUT http://localhost:8080/api/v1/admin/movies/1 \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Inception",
"description": "A mind-bending thriller",
"releaseYear": 2024,
"movieType": "SINGLE",
"movieStatus": "PUBLISHED",
"isPremiumOnly": false
}'curl http://localhost:8080/api/v1/movies/1- Import
postman_collection.json - Import
postman_environment.json - Select environment "Movie Streaming API - Local"
- Run requests in order
# Set variables
BASE_URL="http://localhost:8080"
API_PREFIX="/api/v1"
# Register
curl -X POST $BASE_URL$API_PREFIX/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"test","email":"test@example.com","password":"pass123","fullName":"Test"}'
# Login
curl -X POST $BASE_URL$API_PREFIX/auth/login \
-H "Content-Type: application/json" \
-d '{"usernameOrEmail":"test","password":"pass123"}'
# Get profile
curl $BASE_URL$API_PREFIX/users/me \
-H "Authorization: Bearer YOUR_TOKEN"
# List movies
curl $BASE_URL$API_PREFIX/movies- Create new collection
- Add base URL:
http://localhost:8080 - Add requests for each endpoint
- Use environment variables for tokens
| Category | Count | Auth Required |
|---|---|---|
| Authentication | 7 | Mixed |
| Movies (Public) | 4 | No |
| Movies (Admin) | 5 | Yes (ADMIN) |
| Users | 8 | Yes (Mixed) |
| Total | 24 | - |
users- User accountsmovies- Movie informationepisodes- Movie episodesrefresh_tokens- JWT refresh tokenspassword_reset_tokens- Password reset tokens
Users ← Refresh Tokens
← Password Reset Tokens
Movies → Episodes
→ Ratings
→ Reviews
✅ JWT Authentication - Secure token-based auth ✅ Role-Based Access - ADMIN vs USER roles ✅ Password Hashing - BCrypt encryption ✅ CORS Protected - Cross-origin request handling ✅ Input Validation - Request validation on all endpoints ✅ SQL Injection Prevention - JPA parameterized queries ✅ Rate Limiting Ready - Can add rate limiting
- Pagination support for list endpoints
- Index on frequently queried fields
- Lazy loading for relationships
- Query optimization with JPA projections
- Advanced movie search with filters
- User ratings and reviews
- Wishlist functionality
- Watch history tracking
- Subtitle management
- Streaming quality options
- Payment integration
- Recommendation engine
Solution:
- Ensure token is set:
Authorization: Bearer TOKEN - Check token not expired
- Refresh token using
/auth/refreshendpoint
Solution:
- Verify user has ADMIN role
- Check authentication is present
- Confirm ADMIN token is used (not regular user token)
Solution:
- Verify movie exists
- Check movie is published (visible to users)
- Use correct movie ID or slug
Solution:
- Use unique slug for each movie
- Slugs are case-insensitive
- Can't update existing movie slug
Solution:
- Ensure MySQL is running
- Check database port (default: 3306)
- Verify database credentials in environment
- API Docs: Available on
/swagger-ui.html(if Swagger enabled) - Issues: Report bugs with detailed error messages
- Postman Collection: Use provided collection for easy testing
- Environment: Use provided environment file for quick setup
Proprietary - Movie Streaming Platform
Last Updated: April 6, 2026 Version: 1.0.0 Status: Preparing