
Hello, fellow developers! Today, I'm excited to share my experience building a robust Movie Database API using Laravel. This project showcases the power of Laravel for creating RESTful APIs and demonstrates best practices in API development, testing, and authentication.
The goal was to create an API that allows users to manage a movie database. The main features include:
- Creating new movies
- Retrieving movie details
- Updating existing movies
- Deleting movies
- User authentication and authorization
- Manage user's "Watch Later" list
I started by setting up a new Laravel project and configuring the database. Laravel's built-in tools made this process smooth and straightforward.
sshCopycomposer create-project laravel/laravel movie-database-api cd movie-database-api php artisan migrate
The database structure includes the following main tables:
- users for storing user information
- movies for storing movie details
- genres for categorizing movies
- movie_user for managing "watch later" list
Here's an example of the migration for the movies table:
phpCopypublic function up() { Schema::create('movies', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('description'); $table->date('release_date'); $table->foreignId('genre_id')->constrained(); $table->timestamps(); }); }
I created Eloquent models for each of the main entities. Here's an example of the Movie model:
phpCopy// app/Models/Movie.php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Movie extends Model { use HasFactory; protected $fillable = ['title', 'description', 'release_date', 'genre_id']; public function genre() { return $this->belongsTo(Genre::class); } }
The API endpoints were created using Laravel's resource controllers. Here's an example of the store method in the MovieController :
phpCopy// app/Http/Controllers/MovieController.php public function store(Request $request) { $validatedData = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'required|string', 'release_date' => 'required|date', 'genre_id' => 'required|exists:genres,id', ]); $movie = Movie::create($validatedData); return response()->json($movie, 201); }
I defined the API routes in the routes/api.php file. Here's a snippet of the route configuration:
phpCopy// routes/api.php use App\Http\Controllers\MovieController; Route::middleware('auth:sanctum')->group(function () { Route::apiResource('movies', MovieController::class); Route::post('movies/{movie}/watch-later', [MovieController::class, 'addToWatchLater']); Route::delete('movies/{movie}/watch-later', [MovieController::class, 'removeFromWatchLater']); Route::get('watch-later', [MovieController::class, 'getWatchLaterList']); });
Here's a breakdown of the main API endpoints:
Authentication Endpoints:
-
POST
/register
- Description: Register a new user
- Request Body: JSON object with user details (name, email, password)
- Response: JSON object with user details and access token
- Status Code: 201 Created
-
POST
/login
- Description: Authenticate a user and get an access token
- Request Body: JSON object with email and password
- Response: JSON object with user details and access token
- Status Code: 200 OK or 401 Unauthorized
-
POST
/logout
- Description: Logout the authenticated user (revoke the token)
- Response: JSON object with logout confirmation
- Status Code: 200 OK
- Authentication: Requires a valid access token
Movie Endpoints:
-
GET
/movies
- Description: Retrieve all movies
- Response: JSON array of movie objects
- Authentication: Requires a valid access token
- Status Code: 200 OK
-
POST
/movies
- Description: Create a new movie
- Request Body: JSON object with movie details (title, description, release_date, genre_id)
- Response: JSON object of the created movie
- Authentication: Requires a valid access token
- Status Code: 201 Created
-
GET
/movies/{id}
- Description: Retrieve a specific movie by ID
- Response: JSON object of the requested movie
- Authentication: Requires a valid access token
- Status Code: 200 OK or 404 Not Found
-
PUT
/movies/{id}
- Description: Update an existing movie
- Request Body: JSON object with movie details to update
- Response: JSON object of the updated movie
- Authentication: Requires a valid access token
- Status Code: 200 OK or 404 Not Found
-
DELETE
/movies/{id}
- Description: Delete a specific movie
- Response: JSON object with deletion status
- Authentication: Requires a valid access token
- Status Code: 200 OK or 404 Not Found
-
POST
/movies/{movie}/watch-later
- Description: Add a movie to the user's watch later list
- Response: JSON object with confirmation message
- Authentication: Requires a valid access token
- Status Code: 200 OK or 409 Conflict if already in list
-
DELETE
/movies/{movie}/watch-later
- Description: Remove a movie from the user's watch later list
- Response: JSON object with confirmation message
- Authentication: Requires a valid access token
- Status Code: 200 OK or 404 Not Found if not in list
-
GET
/watch-later
- Description: Retrieve the user's watch later list
- Response: JSON object with user details and list of movies
- Authentication: Requires a valid access token
- Status Code: 200 OK
For user authentication, I implemented Laravel Sanctum. This provides a lightweight authentication system for SPAs, mobile applications, and simple token-based APIs.
phpCopyprotected $middlewareGroups = [ // ... 'api' => [ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 'throttle:api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ];
One of the most crucial parts of this project was implementing comprehensive tests. Here's an example of a test for adding a movie:
phpCopy// tests/Feature/MovieTest.php test('user can add a movie to database', function () { Sanctum::actingAs($this->user); $genre = Genre::first(); $movieData = [ 'title' => 'Test Movie', 'description' => 'This is a test movie description', 'release_date' => '2023-01-01', 'genre_id' => $genre->id, ]; $response = $this->postJson('/api/movies', $movieData); $response->assertStatus(201) ->assertJsonStructure([ 'id', 'title', 'description', 'release_date', 'genre_id', 'created_at', 'updated_at', ]); $this->assertDatabaseHas('movies', [ 'title' => 'Test Movie', 'description' => 'This is a test movie description', 'release_date' => '2023-01-01', 'genre_id' => $genre->id, ]); });
Another example of a test that checks if the user can remove a movie from the watch later list:
phpCopytest('user can remove a movie from watch later list', function () { Sanctum::actingAs($this->user); $movie = $this->movies->first(); $this->user->watchLater()->attach($movie->id); $response = $this->deleteJson("/api/movies/{$movie->id}/watch-later"); $response->assertStatus(200) ->assertJson([ 'message' => "The movie \"{$movie->title}\" has been removed from your watch later list." ]); $this->assertDatabaseMissing('movie_user', [ 'user_id' => $this->user->id, 'movie_id' => $movie->id, ]); });

To help you get started with testing our Movie Database API, I'll walk you through the process of using Postman to interact with some of our endpoints. Postman is a popular API client that makes it easy to send requests and analyze responses.
Base URL
For all these examples, we'll be using the following base URL:
textCopyhttps://libe.dev/demo/movie-api/v1/api
Make sure to prepend this base URL to all the endpoints we'll be testing.
-
User Registration
Let's start by registering a new user:
- Method: POST
- Endpoint: /register
-
Headers:
- Content-Type: application/json
-
Body (raw JSON):jsonCopy
{ "name": "John Doe", "email": "john@example.com", "password": "securepassword123" }
Upon successful registration, you should receive a response with the user's details and an access token. -
User Login
Now, let's log in with the newly created user:
- Method: POST
- Endpoint: /login
-
Headers:
- Content-Type: application/json
-
Body (raw JSON):jsonCopy
{ "email": "john@example.com", "password": "securepassword123" }
The response should include an access token. Copy this token as we'll need it for authenticated requests.
-
Creating a Movie
Let's create a new movie entry:
- Method: POST
- Endpoint: /movies
-
Headers:
- Content-Type: application/json
- Authorization: Bearer {your_access_token}
-
Body (raw JSON):jsonCopy
{ "title": "Inception", "description": "A thief who enters the dreams of others to steal secrets from their subconscious.", "release_date": "2010-07-16", "genre_id": 1 }
You should receive a response with the details of the newly created movie.
-
Adding a Movie to Watch Later
Finally, let's add a movie to our Watch Later list:
- Method: POST
- Endpoint: /movies/{movie_id}/watch-later
-
Headers:
- Authorization: Bearer {your_access_token}
Replace {movie_id} with the ID of the movie you want to add to your Watch Later list.
You should receive a confirmation that the movie has been added to your Watch Later list.
-
Data Validation
- I implemented detailed validation rules in the request classes:
phpCopy
public function rules() { return [ 'title' => 'required|string|max:255', 'description' => 'required|string', 'release_date' => 'required|date', 'genre_id' => 'required|exists:genres,id', ]; }
- Authentication - Integrating Sanctum required careful configuration in the config/sanctum.php file.
-
Testing Edge Cases
- Writing tests for various scenarios helped identify and fix potential issues early:
phpCopy
// tests/Feature/MovieTest.php test('adding a movie with invalid data', function () { Sanctum::actingAs($this->user); $invalidData = [ 'title' => '', 'description' => '', 'release_date' => 'not-a-date', 'genre_id' => 'not-a-number', ]; $response = $this->postJson('/api/movies', $invalidData); $response->assertStatus(422) ->assertJsonValidationErrors(['title', 'description', 'release_date', 'genre_id']); });
- The importance of TDD: Writing tests before implementing features helped me think through the API design more thoroughly.
- Laravel's ecosystem: The project showcased how Laravel's built-in tools and packages can significantly speed up API development.
- API design principles: I learned the importance of consistent response structures and proper HTTP status code usage.
While the current implementation is solid, there's always room for improvement:
- Implement pagination for listing movies
- Add more advanced filtering and sorting options
- Integrate with a third-party movie database API for additional information
- Implement caching to improve performance
Developing this Movie Database API with Laravel highlighted the framework’s intuitive syntax and robust capabilities. This project showcases how to build a secure, well-tested API that can support a range of front-end applications.
I hope this guide gives you an insight into Laravel’s potential for API development. Happy coding!
