Create CRUD REST API with Laravel API Resource and document with Swagger

Kit Loong
6 min readJun 7, 2020
Laravel REST API with Swagger

Note: This project uses Laravel 10 at the time of writing.

What is your common practice to code REST API with Laravel?

Recently I received the above question from my new members in one REST API project. After some discussion and brainstorming, I found that we have different opinions toward the implementation.

Laravel is a powerful PHP framework with a load of features with elegant syntax, allowing developers to achieve the same result with different approaches. In this article, I am going to summarize my implementation to create a REST API with this great framework.

Please note these are based on my practice and preference, by all means, I welcome different approaches.

To see it in action you could clone the repository here: https://github.com/kitloong/laravel-api-practice, by following the setup instruction from the README, you can run the API example within a minute.

If you would like to set up the project from scratch by yourself, please go ahead to install a fresh new Laravel project, then follow the remainder of this article.

I strongly recommend you set up your project with Sail, a built-in solution for running your Laravel project using Docker.

During installation, you should at least your favorite database (in my case, MySQL) when Sail is prompted to ask for input additional services.

Finally, start Sail to serve your application.

./vendor/bin/sail up

Migration and Data Seed

Navigate to database/seeders/DatabaseSeeder.php and uncomment the user seed. Your final result should look like this:

/**
* Seed the application's database.
*/
public function run(): void
{
\App\Models\User::factory(50)->create();

// \App\Models\User::factory()->create([
// 'name' => 'Test User',
// 'email' => 'test@example.com',
// ]);
}

Run migration to set up database tables and seed data for development purpose.

./vendor/bin/sail artisan migrate
./vendor/bin/sail artisan db:seed

Create an API Controller

After the installation, create an API Resource controller by:

php artisan make:controller Api/UserController -m User --api
  1. Api/UsersController : Create UserController at directory app/Http/Controllers/Api
  2. -m User : Bind UsersController to model User
  3. --api : Generate a resource controller that excludes create and edit methods.

Define API route

Next, define our API routes in route/api.php with the following code:

use App\Http\Controllers\Api\UserController;

Route::apiResource('users', UserController::class);

Please note the above code is identical to:

use App\Http\Controllers\Api\UserController;

Route::get('users', [UserController::class, 'index'])->name('users.index');
Route::post('users', [UserController::class, 'store'])->name('users.store');
Route::get('users/{user}', [UserController::class, 'show'])->name('users.show');
Route::match(['put', 'patch'], 'users/{user}', [UserController::class, 'update'])->name('users.update');
Route::delete('users/{user}', [UserController::class, 'destroy'])->name('users.destroy');

Create API Resource

API resource is a transformation layer that sits between your Eloquent models and the JSON responses returned to your application’s users.

If we are going to develop an API application, we should utilize API resources. The following command creates an UserResource(return a single resource) and an UserCollection (return a collection of resources).

php artisan make:resource UserResource
php artisan make:resource UserCollection

Next, navigate to UserController to complete the CRUD methods.

List action

To return records with pagination links and meta about the paginator’s state:

public function index(): JsonResponse
{
$users = User::paginate();
return (new UserCollection($users))->response();
}

API

(Assuming the service is running on port 8080)

curl http://localhost:8080/api/users \
-H 'Accept: application/json'

Output

{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28@example.com",
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort@example.com",
},
...
],
"links":{
"first": "http://localhost:8080/api/users?page=1",
"last": "http://localhost:8080/api/users?page=1",
"prev": null,
"next": null
},
"meta":{
"current_page": 1,
"from": 1,
"last_page": 1,
"links": [
{
"url": null,
"label": "« Previous",
"active": false
},
{
"url": "http://localhost:8080/api/users?page=1",
"label": "1",
"active": true
},
...
]
"path": "http://localhost:8080/api/users",
"per_page": 15,
"to": 10,
"total": 10
}
}

The output explains itself however if you wish to know more in-depth, feel free to read documented details here.

Get action

To get a single record:

public function show(User $user): JsonResponse
{
return (new UserResource($user))->response();
}

API

curl http://localhost:8080/api/users/1 \
-H 'Accept: application/json'

Output

{
"data": {
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28@example.com",
}
}

Create action

Create a user with validation. The success response should return the status code 201 :

public function store(Request $request): JsonResponse
{
$request->validate([
'name' => 'bail|required|string|max:255',
'email' => 'bail|required|string|max:255|email|unique:users,email',
'password' => 'bail|required|string|min:8'
]);

$user = new User();

$user->name = $request->input('name');
$user->email = $request->input('email');
$user->password = Hash::make($request->input('password'));
$user->save();

Log::info("User ID {$user->id} created successfully.");

return (new UserResource($user))->response()->setStatusCode(Response::HTTP_CREATED);
}

API

curl -X POST http://localhost:8080/api/users \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d $'{
"name": "Name",
"email": "test@email.com",
"password": "password"
}'

Output

{
"data": {
"id": 2,
"name": "Name",
"email": "test@email.com",
}
}

Validation error

When using the validate method during an AJAX request, Laravel will not generate a redirect response. Instead, Laravel generates a JSON response containing all of the validation errors

Laravel uses the method expectsJson to determine if the JSON response should be used.

// @see https://github.com/laravel/framework/blob/10.x/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php#L24
public function expectsJson()
{
return ($this->ajax() && ! $this->pjax() && $this->acceptsAnyContentType()) || $this->wantsJson();
}

To summarize, Laravel will know it is a JSON response when the following conditions are met:

  1. Your Request Header contains X-Requested-With: XMLHttpRequest &&
  2. The header does not contain X-PJAX &&
  3. Header contains Accept: */*

Or

  1. Accept Header contains /json or +json , such as Accept: application/json

This article uses Accept: application/json .

curl -X POST http://localhost:8080/api/users \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d $'{
"name": "Name"
}'

Error output

{
"message": "The email field is required. (and 1 more error)",
"errors": {
"email": [
"The email field is required."
],
"password": [
"The password field is required."
]
}
}

Update action

Success request should return status code 200 or 204 . In my case, I prefer to return 200 with updated user data.

public function update(Request $request, User $user): JsonResponse
{
$request->validate([
'name' => 'bail|required|string|max:255',
'email' => 'bail|required|string|max:255|email|unique:users,email,'.$user->id
]);

$user->name = $request->input('name');
$user->email = $request->input('email');
$user->save();

Log::info("User ID {$user->id} updated successfully.");

return (new UserResource($user))->response();
}

API

curl -X PUT http://localhost:8080/api/users/2 \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d $'{
"name": "New Name",
"email": "test@email.com"
}'

Output

{
"data": {
"id": 2,
"name": "New Name",
"email": "test@email.com",
}
}

Delete action

Lastly, a delete action should return the status code 204 with an empty body.

public function destroy(User $user): Response
{
$user->delete();

Log::info("User ID {$user->id} deleted successfully.");

return response(null, Response::HTTP_NO_CONTENT);
}

API

curl -X DELETE http://localhost:8080/api/users/2 \
-H 'Accept: application/json'

Output

Empty

Response status code

Here is a recap of each API status code

  • List — 200
  • Get — 200
  • Created — 201
  • Updated — 200 or 204
  • Deleted — 204

Documentation in Swagger

Detailed documentation helps your team members or users know your API's details, expected parameter requests, and their associated success and error responses.

Swagger simplified API development helps you design and document your APIs at scale.

https://github.com/DarkaOnLine/L5-Swagger excelled as a wrapper of Swagger-php and swagger-ui adapted to work with Laravel.

I went ahead with the Swagger-php attributes implementation, and I am very satisfied with the result. Attributes are tidier and type-safe compared to Annotations.

It is easier to follow by browsing the source code directly:

  1. Define the User schema.
  2. Define the paginate resource schema.
  3. Define the User resource and collection schemas.
  4. Define the Swagger API, base URL, and project description.
  5. Define every endpoint from the controller.

Finally, generate Swagger JSON by:

php artisan l5-swagger:generate

Open http://localhost:8080/api/documentation with your favorite browser to browse the Swagger page. You should see a page similar to below screenshot:

Swagger UI

Logging

Very crucial to analyze, when an error occurred, the log is the first place to start your investigation.

Laravel provides robust logging services that allow you to log messages to files. https://github.com/kitloong/laravel-app-logger built on top of Laravel logging service gives your API application the ability to log:

HTTP request

What are the incoming requests?

[2021-01-10 23:35:27] local.INFO: 2725ffb10adeae3f POST /path - Body: {"test":true} - Headers: {"cookie":["Phpstorm-12345"],"accept-language":["en-GB"]}

Performance

How long is the response time?

[2021-01-10 23:35:27] local.INFO: 2725ffb10adeae3f POST /path - Time: 55.82 ms - Memory: 22.12 MiB

Query

What query has been executed?

[2021-01-10 23:35:27] local.INFO: Took: 2.45 ms mysql Sql: select * from `users` where `id` = 1

Conclusion

All code examples (including Swagger and logging) from this article are available at https://github.com/kitloong/laravel-api-practice.

I hope this article can help your REST API development.

Happy coding!

References

  1. https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
  2. https://laravel.com/docs/eloquent-resources
  3. https://laravel.com/docs/controllers#api-resource-routes
  4. https://github.com/DarkaOnLine/L5-Swagger
  5. https://github.com/kitloong/laravel-app-logger

--

--