Home » Laravel » How to Create REST API in Laravel

How to Create REST API in Laravel

Modern websites are often developed in the SPA (Single Page Application) paradigm, when there is a separate backend written in PHP or another backend language and a frontend developed using one of the popular JavaScript frameworks, such as Vue or React. The backend implements only the API interface that the frontend uses to receive data and perform the necessary actions.

Backend developers often use the REST approach to create their APIs. This approach was introduced in 1994 with HTTP protocol but is still very popular. In this article, I will show you what REST is and how to create an RESTful API in Laravel using a simple application that can manage a list of tasks.


Table of Contents

What is REST?

REST, stands for Representational State Transfer, is a set of rules that allows you to structure and standardize the interaction of web services over the HTTP protocol. In this article, I will explain only some of the HTTP protocol’s features and REST’s features. However, here are the main ideas of REST that we will use:

  • Stateless – all REST requests must be independent of each other. The server should not store any states between requests. The server and client should understand the request and the response without knowing anything from previous requests.
  • Use HTTP-verbs – the HTTP request should be clear about what action the server should perform. You should use the HTTP verb “GET” to get resources or data (but not modify), “POST” to create resources, “PUT” to modify resources, “PATCH” to partially modify resources, and “DELETE” to remove the resource.
  • Manage resources, not commands – you shouldn’t think of API endpoints as commands. Think of them as actions on resources. Each endpoint must perform an action on a specific resource. The resource name must be present in the request path. For example, GET: /tasks, DELETE /tasks/1, etc.
  • Return status code – endpoint must return the correct status code with the response. Return 200 if the request was successful, 201 if the resource was created, 204 if the response is empty, for example, the resource was deleted, 403 if there were problems with access rights, 404 if the resource was not found, and 500 if there were other server errors.

Laravel has everything you need to implement the above. In addition, there are a few rules for better organizing a project to make it easy for other developers to understand and maintain. I’ll show you everything on the sample Task project. Our API will be able to create tasks, update and delete them, and display a list of all tasks with pagination. I will also show you how to write tests for such APIs using PHPUnit.

Application Structure

First, we need a migration that will create a tasks table in the database. Next, we need a Task model which will be used for interaction with the tasks table. Additionally, we will create a TaskFactory factory to make fake tasks for testing.

Then, we need a controller class to handle user requests and perform actions on tasks. However, there are a few peculiarities. Previously, placing most of the logic and validation directly in the controller was typical. However, this is not quite right. According to the Single responsibility principle with SOLID, each object should solve only a specific task. Therefore, We will use a separate controller for each action in this article. We will use a separate class extended from the FormRequest to validate the data in the request. Also, the formatting of the response will be placed in the class extended from the JsonResource or CollectionResource class.

All the business logic of working with tasks will be implemented in the TaskService class. This decomposition will keep each class simpler and smaller and ensure that each class has its own area of responsibility. For greater convenience in transferring data between classes, in this case, the name and description of the task, we will use a Data Transfer Object called TaskDto. This is more convenient than passing everything in separate variables or an array. Of course, in this little project, it is unnecessary, but it can be helpful in large projects.

Here, I will skip all the steps necessary to create a project from scratch, and I will not describe API authentication since the article will be complex and large anyway. The topic of authorization will be covered in a separate article. So we’ll need the following classes:

  • Task – task model class;
  • IndexController – controller that allows to get list of tasks;
  • NewController – controller that allows to create a new task;
  • UpdateController – controller that updates a task;
  • DeleteController – controller that deletes a task;
  • TaskRequest – request class that validates data in update or create request;
  • TaskResource – format information about a task;
  • TaskListResource – format information about a list of tasks;
  • TasksService – service class which contains all code related to creating, fetching, and other actions to tasks;
  • TaskDto – class that contain information about a task.

In this article, I will use curl to make requests to the API for the convenience of displaying information on screenshots, but I usually use Postman in my work.

How to Create an API with Laravel

1. Handling Exceptions

Before we start, let’s look at exception handling in the Laravel API. When an exception is thrown by default, the data will be returned in JSON format only if the client sends the Accept: application/json header. Otherwise, Laravel will redirect you to the previous page, and you won’t know where the problem is.

You can configure Laravel always to return JSON for API routes. The best way to do it – open the app/Exceptions/Handler.php file and add the shouldReturnJson() method with the following content:

app/Exceptions/Handler.phpprotected function shouldReturnJson($request, Throwable $e): bool { return parent::shouldReturnJson($request, $e) || $request->is("api/*"); }

This method will override the method from the base class, and after that, all errors for routes that contain the /api* string will be returned in JSON format.

1. Creating a Migration and Model

Now let’s create a migration for the tasks table in the database. Run the following command in the command line:

php artisan make:migration CreateTasksTable

For each task, we’ll need two fields besides the standard ID and timestamps: a name and a description. Since the tasks will be publicly available, using UUID as an identifier is better. The code for creating the table will look like this. It should be placed in the up() method of the migration class:

database/migrations/2023_07_02_093831_create_tasks_table.phpSchema::create("tasks", function (Blueprint $table) { $table->uuid("id")->primary(); $table->string("name"); $table->text("description"); $table->timestamps(); });

After that, run the following command to apply the migrations:

php artisan migrate

Next, you need to create a Task model using the following command:

php artisan make:model Task

Since the UUID will be used as an identifier, you need to add the HasUuids trait to Task model:

app/Models/Task.phpuse Illuminate\Database\Eloquent\Concerns\HasUuids;

app/Models/Task.phpuse HasUuids;

Also, you should add the $fillable field to the model with a list of fields that can be filled from the array:

app/Models/Task.phppublic $fillable = ['name', 'description'];

After that, you can call the fill() method in the model or create a new model object and pass the array with data to the constructor.

2. Creating a Factory

To be able to fill the database with test data when running tests, you need to create a factory for the Task model. Run the following command:

php artisan make:factory TaskFactory

After that, it is enough to add the code for making values for the model fields to the definition() method. To generate random data each time, you can use the fake() helper. For example.

database/factories/TaskFactory.phppublic function definition(): array { return [ "id" => fake()->uuid(), "name" => fake()->text(100), "description" => fake()->realText(1000), ]; }

Next, you can create a seeder class that will fill your database with records for testing:

php artisan make:seeder TaskSeeder

Then, add the following code to create records to the run() method in the seeder. For example:

database/seeders/TaskSeeder.phppublic function run(){ // Create one task with the specific id \App\Models\Task::factory([ "id" => "eb15f61d-6b54-49f6-b90a-4a370d7b4c70" ])->create(); // Create 20 tasks with random data \App\Models\Task::factory()->count(20)->create(); }

After that, run the seeder, and several new records will appear in the database:

php artisan db:seed Database\\Seeders\\TaskSeeder

Later, I will show you how to use this same factory directly in test classes.

3. Creating a Service

The developers from Spatie recommend using a separate Action class for each action you want to perform. It is enough to group action methods related to the same functionality in one service class to avoid placing everything in the controller.

Let’s create a service that retrieves data from the database and returns it to the controller. Of course, in this case, when the logic is straightforward, you could call the methods of the Eloquent model in the controller. However, when there is much more logic in more complex projects, it is better to put everything in separate classes. There is no command for creating services, but you can create a TaskService class in your IDE:

app/Services/TaskService.php<?php namespace App\Services; //Import classes that we will use later use App\DataTransfers\TaskDto; use App\Models\Task; use Illuminate\Pagination\LengthAwarePaginator; class TaskService { //methods }

We will add methods later as we implement the functionality, but now I want to show you one feature. By default, you will get a new object of a service class each time when you request it using dependency injection. However, having only one class object is recommended to reduce memory usage. The service should be registered as a singleton in service provider. Find the register() method of the AppServiceProvider and add the following code:

app/Providers/AppServiceProvider.phppublic function register(): void { $this->app->singleton(\App\Services\TaskService::class, function () { return new \App\Services\TaskService(); }); }

Please note that here you should either write the full namespace to the class or import it using the use statement at the beginning of the file. Otherwise, it will not work, and you will not get an error. After that, you can fetch the service class using dependency injection, which will always be the same object.

4. Retrieving a Single Task

Let’s first implement an endpoint to get information about a single task. Add the getTaskById() method to the service with the following code:

app/Services/TaskService.phppublic function getTaskById(string $id): Task { //Returns Task model with specified $id or throws NotFoundHttpException return Task::query()->findOrFail($id); }

After that, we could send the entire model content to the client. However, some fields are supposed to be private. Therefore, it is better to create a resource that will take the model and convert it to a JSON resource, selecting only the necessary fields. There is a command make:resource for that purpose:

php artisan make:resource TaskResource

After executing this command, the file app/Http/Resources/TaskResource.php will appear. By default, it has a toArray() method, where you need to provide the data you want to display. For example, we will display only id, name, and description. In the constructor, this class takes a resource. It can be anything. In this case, it is a model. And in the object itself, this resource will be available in the $resource field:

app/Http/Resources/TaskResource.phppublic function toArray(Request $request): array { //The same resource class that is passed in __construct() return [ "id" => $this->resource->getKey(), "name" => $this->resource->name, "description" => $this->resource->description, ]; }

Also, by default, all data will be returned in the data array. Add the following field to the resource file to turn off the wrapping:

app/Http/Resources/TaskResource.phppublic static $wrap = null;

Now, we must create a controller to call methods from all these classes and send the response to the frontend. We will use the following endpoint: GET /task/{id} to get information about a task by id. Laravel allows you to get the values of parameters parsed from the path using dependency injection. Our service can also be obtained with dependency injection. Therefore, the code will look like this:

app/Http/Controllers/GetTaskController.php<?php namespace App\Http\Controllers; use App\Http\Resources\TaskResource; use App\Services\TaskService; class GetTaskController extends Controller { public function __invoke(string $id, TaskService $service) { return new TaskResource($service->getTaskById($id)); } }

Now we need to register this route in the file routes/api.php:

routes/api.phpRoute::get("/tasks/{id}", \App\Http\Controllers\GetTaskController::class);

Since all the code in the controller is in the __invoke() method, this controller can be executed as a function, and the method name can be written here. After that, you can ensure that it works using Postman or cURL in the terminal:

curl -s http://localhost/api/tasks/eb15f61d-6b54-49f6-b90a-4a370d7b4c70 | json_pp

The json_pp command in this example is used to beautify the JSON responses.

5. Retrieving a List of Tasks

You have 20 records in the database created in the previous step, and now you can fetch them using the public API. You shouldn’t return all records from the database in one request. Usually, you only need about ten items on the page. Otherwise, if there are a lot of records, it will create a heavy load on the server and the network. Transfer only the data that is necessary. Eloquent supports pagination by default, so you don’t need to implement anything additional here. It is enough to call the paginate() method instead of get() to get the paginated list of models.

Add the TaskService method getTasks() to the TaskService that will return a list of models for the specified page. Eloquent reads the page parameter from the request, so we need to specify how many tasks we want to return per page:

app/Services/TaskService.phppublic function getTasks(): LengthAwarePaginator { return Task::query()->paginate(10); }

To display the list, we also need a resource, not JSON, but a Collection. It can be created using the following command:

php artisan make:resource TaskListResource --collection

Here, everything works in much the same way as in TaskResource. This class will take our collection of tasks and convert it into an array with information about the resources, where each resource will be processed by the same TaskResource. In general, you can simplify it by using the static collection() method from JsonResource, which will do all the work for you, but I want to show you how to create dedicated resources. So the resource will contain the following code:

app/Http/Resources/TaskListResource.php<?php namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\ResourceCollection; class TaskListResource extends ResourceCollection { public function toArray(Request $request): array { return [ "tasks" => $this->resource->map(function($task) use ($request){ return (new TaskResource($task))->toArray($request); }) ]; } }

In this case, we don’t turn off wrapping the result in a data array because this is necessary so that we can insert pagination metadata. The root element will contain the data and meta fields. And the data array will contain a list of tasks tasks. There may also be other metadata that you need. Now create a controller to display the list of resources:

app/Http/Controllers/ListTasksController.php<?php namespace App\Http\Controllers; use App\Http\Resources\TaskListResource; use App\Services\TaskService; class ListTasksController extends Controller { public function __invoke(TaskService $service){ return new TaskListResource($service->getTasks()); } }

And register it in routes/api.php. In REST, this will also be a GET request, but with the path /tasks:

routes/api.phpRoute::get("/tasks", \App\Http\Controllers\ListTasksController::class);

The list will look like this:

curl -s http://localhost/api/tasks | json_pp

6. Creating a Task

We need to get the task name and description from the frontend to create a new task. Since this is a data change, the request type will be POST, and the data will be in the request’s body. As I wrote above, I want to keep the controllers as small as possible, so all the validation and processing of the request data should be done in a separate class. In Laravel, you can create a class inheriting from FormRequest. You can read more about this in the official documentation. Use the following command to create TaskRequest class:

php artisan make:request TaskRequest

There are two methods to pay attention to. The first one is authorize() , which is usually used to authorize requests. Since we don’t need authorization right now, we can just always return true:

app/Http/Requests/TaskRequest.phppublic function authorize(): bool { return true; }

The second method is rules(), in which we need to write rules for validating the data that comes from the user. Validation for creating a task may look like this:

app/Http/Requests/TaskRequest.phppublic function rules(): array { return [ "name" => ["required", "string", "max: 200"], "description" => ["required", "string", "max:1000"], ]; }

Now, we can simply call the validated() method on the TaskRequest object in the controller and get all the validated data. But that’s not all. There can be a lot of data in the request, and it is not convenient to work with an array. It is much more convenient when there is a Data Transfer Object where the fields are strongly typed and the IDE can show you their name. Let’s create a TaskDto and return it from our TaskRequest class instead of an array with data:

app/DataTransfers/TaskDto.php<?php namespace App\DataTransfers; use Illuminate\Support\Carbon; class TaskDto { public function __construct( public readonly string $name, public readonly string $description ) {} }

Another advantage of this approach is converting data to the desired format. You can change the data before passing it into the DTO constructor. Add a method to TaskRequest that will return TaskDto:

app/Http/Requests/TaskRequest.phpuse App\DataTransfers\TaskDto;

app/Http/Requests/TaskRequest.phppublic function getTaskData(): TaskDto { $data = $this->validated(); $dto = new TaskDto( name: $data["name"], description: $data["description"] ); return $dto; }

Here, the validated() method is used to get all the validated data. It returns an array that contains the name and description fields since they are marked as required in the validation rules. Please note that if no rules are specified for a field in rules(), then it will not be included in validated() return.

Next, let’s add a newTask() method to the service that will create a new task based on TaskDto:

app/Services/TaskService.phppublic function newTask(TaskDto $data): Task { $task = new Task(); $task->fill([ "name" => $data->name, "description" => $data->description, ]); $task->save(); return $task; }

It remains to create a controller that will receive data, call the service, and then return information about the newly created task:

app/Http/Controllers/CreateTaskController.php<?php namespace App\Http\Controllers; use App\Http\Requests\TaskRequest; use App\Http\Resources\TaskResource; use App\Services\TaskService; class CreateTaskController extends Controller { public function __invoke(TaskRequest $request, TaskService $service) { return new TaskResource( $service->newTask($request->getTaskData()) ); } }

And register this route in routes/api.php:

routes/api.phpRoute::post("/tasks", \App\Http\Controllers\CreateTaskController::class);

This is what the response will look like when the task is successfully created:

curl -s -X POST http://localhost/api/tasks  -H "Content-Type: application/json" -d '{"name": "Simple task", "description": "Simple description"}' | json_pp

And this is what it looks like if there was an error:

curl -s -X POST http://localhost/api/tasks  -H "Content-Type: application/json" -d '{"name": "Simple task"}' | json_pp

7. Update the Task

You can use the same TaskRequest class to update the task. This should be a PUT request. The new data will be in the request’s body, and the task identifier that should be updated will be in the URL path, just as it was done when displaying information about one task. First, let’s create a method in the service that will perform the update:

app/Services/TaskService.phppublic function updateTask(Task $task, TaskDto $data): Task { $task->fill([ "name" => $data->name, "description" => $data->description ]); $task->save(); return $task; }

And now the controller:

app/Http/Controllers/UpdateTaskController.php<?php namespace App\Http\Controllers; use App\Http\Requests\TaskRequest; use App\Http\Resources\TaskResource; use App\Services\TaskService; class UpdateTaskController extends Controller { public function __invoke( string $id, TaskRequest $request, TaskService $service ) { $task = $service->getTaskById($id); return new TaskResource( $service->updateTask($task, $request->getTaskData()) ); } }

The registration code for this controller will look like this:

routes/api.phpRoute::put("/tasks/{id}", \App\Http\Controllers\UpdateTaskController::class);

And check that everything works:

curl -s -X PUT http://localhost/api/tasks/eb15f61d-6b54-49f6-b90a-4a370d7b4c70  -H "Content-Type: application/json" -d '{"name": "Updated task", "description": "Updated description"}' | json_pp

8. Deleting a Task

Deleting a task also looks similar to getting information about a specific task. First, let’s add a method to the service:

app/Services/TaskService.phppublic function deleteTask(Task $task) { $task->delete(); }

Then, add the following code into the controller:

app/Http/Controllers/DeleteTaskController.php<?php namespace App\Http\Controllers; use App\Services\TaskService; class DeleteTaskController extends Controller { public function __invoke(string $id, TaskService $service){ $task = $service->getTaskById($id); $service->deleteTask($task); return response()->noContent(); } }

routes/api.phpRoute::delete("/tasks/{id}", \App\Http\Controllers\DeleteTaskController::class);

This is how it will work in curl:

curl -I -X DELETE http://localhost/api/tasks/eb15f61d-6b54-49f6-b90a-4a370d7b4c70

Now, you have all the basic CRUD methods for managing tasks built according to the rules of the REST API. We’ve already checked that everything works while writing code using cURL. However, when you make certain changes, you must repeat the testing to ensure nothing is broken.

Testing the REST API

1. Preparing the Environment

Laravel can perform API requests in tests. You can call the methods get(), post(), delete(), etc., directly in the test class if it extends Tests\TestCase. However, first, you need to configure the test environment to execute the tests, not in your main database. Ideally, each test gets a clean database and fills in all the necessary data. The easiest way to do this is to use an in-memory SQLite database. Open the phpunit.xml file and comment out the following lines there:

phpunit.xml<env name="DB_CONNECTION" value="sqlite"/> <env name="DB_DATABASE" value=":memory:"/>

After that, SQLite will be used when running tests, and no changes will be made to the real database.

2. View One Task Test

Now, let’s create a test to display information about a specific task. To do this, you can run the command:

php artisan make:test GetTaskTest

Add the trait Illuminate\Foundation\Testing\RefreshDatabase into the test class to refresh the database before the start of each test case:

tests/Feature/GetTaskTest.php<?php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; use App\Models\Task; class GetTaskTest extends TestCase { use RefreshDatabase; public function testCanGetInfoAboutASpecificTask(): void { //Create one task with random data $task = Task::factory()->create(); //Get info about this task $response = $this->get("/api/tasks/" . $task->getKey()); //Ensure that the data meet expectations $response->assertStatus(200); $response->assertJsonPath("id", $task->getKey()); $response->assertJsonPath("name", $task->name); $response->assertJsonPath("description", $task->description); // If something goes wrong, you can print response body // to find out the reason dump($response->json()); } public function testReturnNotFound(): void { $response = $this->get("/api/tasks/" . fake()->uuid()); $response->assertStatus(404); } }

The first method creates a single task in the database, ensuring that all the necessary information about the task is displayed correctly. The second test verifies that the 404 error works if the task is not found. You can run this test using the following command:

php artisan test --filter GetTaskTest

3. List Tasks Request Test

The next test will check that the list of tasks is displayed correctly. It will do something similar. We will create 30 records in the database and then see how the first and second pages are returned, as well as whether the metadata is counted correctly:

php artisan make:test ListTasksTest

tests/Feature/ListTasksTest.php<?php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; use App\Models\Task; class ListTasksTest extends TestCase { use RefreshDatabase; public function testCanListTasks(): void { Task::factory()->count(30)->create(); $response = $this->get("/api/tasks/"); $response->assertStatus(200); $response->assertJsonPath("meta.current_page", 1); $response->assertJsonPath("meta.total", 30); $response->assertJsonPath("meta.per_page", 10); $data = $response->json(); $this->assertArrayHasKey("data", $data); $this->assertArrayHasKey("tasks", $data["data"]); $this->assertCount(10, $data["data"]["tasks"]); } public function testCanListTasksOnSecondPage(): void { Task::factory()->count(15)->create(); $response = $this->get("/api/tasks?page=2"); $response->assertStatus(200); $response->assertJsonPath("meta.current_page", 2); $response->assertJsonPath("meta.total", 15); $response->assertJsonPath("meta.per_page", 10); $data = $response->json(); $this->assertArrayHasKey("data", $data); $this->assertArrayHasKey("tasks", $data["data"]); $this->assertCount(5, $data["data"]["tasks"]); } }

To run this test, do the following:

php artisan test --filter ListTasksTest

Laravel does a lot of the work for us. All the data returned from the toArray() method is wrapped in the data array since we did not change this name in the $wrap variable. In addition, the meta array was automatically added to the collection with data on pagination and the total number of available records.

4. Create Request Test

In the create request, we must ensure that the required record exists in the database after executing the request.

php artisan make:test NewTaskTest

The test looks like this:

tests/Feature/NewTaskTest.php<?php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class NewTaskTest extends TestCase { use RefreshDatabase; public function testCanCreateANewTask(): void { $data = [ 'name' => fake()->text(100), 'description' => fake()->realText(1000), ]; $response = $this->post('/api/tasks', $data); $response->assertStatus(201); $response->assertJsonPath('name', $data["name"]); $response->assertJsonPath('description', $data["description"]); // Assert that there is a record with a specific name and description // in the tasks table $this->assertDatabaseHas('tasks', [ 'name' => $data["name"], 'description' => $data['description'], ]); } }

Laravel framework understands that this is a creation request and automatically assigns status code 201 instead of the standard 200 without additional settings.

php artisan test --filter NewTaskTest

5. Test the Update Request

In the update request, we will create a task and update it with new data, and then verify that the data has been updated in the database:

php artisan make:test UpdateTaskTest

tests/Feature/UpdateTaskTest.php<?php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; use App\Models\Task; class UpdateTaskTest extends TestCase { use RefreshDatabase; public function testCanUpdateATask(): void { $task = Task::factory()->create(); $data = [ 'name' => fake()->text(100), 'description' => fake()->realText(1000), ]; $response = $this->put("/api/tasks/" . $task->getKey(), $data); $response->assertStatus(200); $response->assertJsonPath("id", $task->getKey()); $response->assertJsonPath("name", $data['name']); $response->assertJsonPath("description", $data['description']); $this->assertDatabaseHas("tasks", [ 'id' => $task->getKey(), 'name' => $data['name'], 'description' => $data['description'], ]); } }

Here is the command that you can use to run the test:

php artisan test --filter UpdateTaskTest

6. Test the Delete Request

The last test is the delete test. Here we need to create a task, delete it using the API and make sure that it no longer exists in the database:

php artisan make:test DeleteTaskTest

tests/Feature/DeleteTaskTest.php<?php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; use App\Models\Task; class DeleteTaskTest extends TestCase { use RefreshDatabase; public function testCanDeleteTask(): void { $task = Task::factory()->create(); $response = $this->delete('/api/tasks/' . $task->getKey()); $response->assertStatus(204); // Assert there is no record with this key in the tasks table $this->assertDatabaseMissing('tasks', [ 'id' => $task->getKey(), ]); } }

php artisan test --filter DeleteTaskTest

Now we can run all the tests and ensure everything works as it should. And every time you make changes to certain parts of the API, you can rerun these tests to make sure that nothing is broken:

php artisan test

Wrapping Up

In this article, I have explained how to create Laravel REST API that allows you to create, edit, add, and delete tasks (CRUD). At this stage, this API is not protected by authorization. You can protect the API with laravel/passport or sanctum. You can read more about this in the article How to create a login API in Laravel. What do you think of this application structure? Do you use something similar or something else? Let me know using the comment form below.

1 thought on “How to Create REST API in Laravel”

Leave a Comment