Build, Test and Document REST APIs in PHP & Laravel — Part I — Building

Jotheeswaran Gnanasekaran
8 min readNov 3, 2021

--

Co-Authored by Regina Sharon G, Santhosh Krishnamoorthy, Swati Akkishetti, Charan Sai & Harsha Sudheer

In this 3 part series, you will be learning how to create RESTful CRUD APIs in PHP & Laravel, Test and Document them using Swagger.

The APIs we will be building will be for a Book Shop. With these APIs, users will be able to Create, Read, Update & Delete books in the Shop.

We will look at how to build these APIs in this instalment. In the subsequent instalments, we will delve into testing and documenting them.

Prerequisites

  • PHP 7.4.24 or Higher
  • PHP Composer
  • Laravel 8.0 or Higher
  • Postman (optional)

To follow along with this tutorial, a basic understanding of the PHP language and Laravel framework is required.

Creating a Laravel Project

To get started, you have to create a Laravel application. To do this you have to run the following command in your terminal:

composer create-project laravel/laravel BookShop

Next, change your current directory to the root folder of the project:

cd BookShop

Start up the Laravel server if it’s not already running:

php artisan serve

You will be able to visit your application on http://127.0.0.1:8000.

If you see this, then all is good, you are ready to move on.

Set Up Database Connection

As we will be using SQLITE as our database for storing books related data, let us configure it in our Laravel application.

Open the .env file in the project folder and update the DB_CONNECTION value to sqlite as below:

DB_CONNECTION=sqlite

Navigate to the BookShop/database folder and create an empty file and name it as database.sqlite.

Setting Up Migrations and Models

With the database configured let us now create the models and database migration files. To do this you have to run:

php artisan make:model Author -m
php artisan make:model Book -m

New files named Authors.php, Books.php will be created in the app/Models directory.

Additionally, migration files will be created in the database/migrations directory to generate our table. You will have to modify the migration files to create a columns for both Books & Author tables.

Open <timestamp>_create_authors_table.php file in the database/migration directory and replace the up() function with the code below:


...
public function up()
{
Schema::create('authors', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('first_name');
$table->string('last_name');
$table->string('email');
});
}
...

Open <timestamp>_create_books_table.php file in the database/migration directory and replace the up() function with the code below:

...
public function up()
{
Schema::create('books', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->integer('price');
$table->unsignedBigInteger('author_id');
$table->foreign('author_id')->references('id')- >on('authors');
});
}
...

Run the following command to apply migrations:

$php artisan migrate

Verify database tables

  • To visualise the table and data available in SQLITE file (database.sqlite), we need a tool so let us download and install DB Browser for SQLite
  • Navigate to the path app/database folder in the project and open database.sqlite with DB Browser for SQLite to view tables and data.

Setting up the Routes

With the database all set up, the next step would be to configure the routes. Routes are basically the HTTP URL locations at which the API endpoints will be available.

Before we do that, let us change the base path for our API urls.

By default, Laravel has a prefix /api/ set for the API urls. This means, your API endpoints will be at http://localhost:8000/api/books. If you prefer to have them at ‘/books’ instead, make the changes in RouteServiceProvider.php file in app/providers/

...
public function boot()
{
$this->configureRateLimiting();

$this->routes(function () {
Route::middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));

Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
});
}
...

Defining the Routes

For our Book Shop application we will need routes for Adding, Updating, Getting and Deleting a book.

Proceed to the routes directory and open the api.php file and create the endpoints as below:

Route::get('/books/{id}', [BookController::class, 'getBook']);
Route::post('/books', [BookController::class, 'addBook']);
Route::put('/books/{id}', [BookController::class, 'updateBook']);
Route::delete('/books/{id}', [BookController::class, 'deleteBook']);

Also, run the following command to verify that the routes are added in properly

php artisan route:list

Note: At this point, BookController is not yet created. Let us create that in the below sections

A look at the project’s folder structure :

Following the common pattern used in application development.

Controllers — contains application logic and passing user input data to service

Services — The middleware between controller and repository. Gather data from controllers, perform validation and business logic, and call repositories for data manipulation.

Repositories — layer for interaction with models and performing DB operations

Models — common laravel model files with relationships defined with database tables

Let’s say, each layer interacts with only single layer at a point

for each DB table there is a model

for each model there is a repository

for each repository there is a service

for each service there is a controller

So basically all layers are 1:1 to each other.

To give example for our case,

BookController -> BookService -> BookRepository -> BookModel

Implement the End points :

Let us now start adding code. Firstly, we shall implement for the ‘Add Book’ endpoint.

Create Repository

Now, let us create the following interface and class files in the Repository folder.

  • IBookRepository php interface file in app/Repository/IBookRepository.php
  • BookRepository php class in app/Repository/BookRepository.php
  • IAuthorRepository php interface file in app/Repository/IAuthorRepository.php
  • AuthorRepository php class in app/Repository/IAuthorRepository.php

IBookRepository:

Declare addBook() method which contains $bookDetails as input parameter and return type as boolean

function addBook($bookDetails): bool;

BookRepository:

Implement addBook() method in BookRepository class as below:

public function addBook($bookDetails): bool
{
return Books::query()->insert($bookDetails);
}

IAuthorRepository:

  • Declare getAuthorIdFromAuthorName() method which contains $authorName as input parameter and return type as integer.
  • Declare addAuthor() method which contains $author details as input parameter and return type as integer which is author id.
function getAuthorIdFromAuthorName($authorName);function addAuthor($author);

AuthorRepository:

  • Implement getAuthorIdFromAuthorName()method in AuthorRepository class.
  • Implement addAuthor() method in AuthorRepository class.
public function getAuthorIdFromAuthorName($authorName)
{
return Authors::query()->where('authors.first_name', '=', $authorName)->first('id');
}
public function addAuthor($author): int
{
return Authors::query()->insertGetId($author);
}

Register application classes

After creating the interface and class of the repository layer , let us bind them in app/Providers/AppServiceProvider.php

...
public function register()
{
$this->app->bind(IBookRepository::class, function (){
return new BookRepository();
});

$this->app->bind(IAuthorRepository::class, function (){
return new AuthorRepository();
});
}
...

Create Service

Next, let us create a service class BookService.php under service folder app/Service

Create the constructor in the service layer(BookService.php) and pass both the repository layers(IBookRepository, IAuthorRepository), as constructor parameters.

private $bookRepository;
private $authorRepository;

public function __construct(IBookRepository $bookRepository, IAuthorRepository $authorRepository)
{
$this->bookRepository = $bookRepository;
$this->authorRepository = $authorRepository;
}

Add the business logic to a service class by creating a new method addBook().

public function addBook($title, $price, $author): string
{

$authorDetails = $this->authorRepository->getAuthorIdFromAuthorName($author['first_name']);

if ($authorDetails == null) {
$authorId =$this->authorRepository->addAuthor($author);
$bookDetails =array('title' => $title, 'price' => $price, 'author_id' => $authorId );

}else{
$bookDetails = array('title' => $title, 'price' => $price, 'author_id' => $authorDetails->id);
}

if ($this->bookRepository->addBook($bookDetails)) {
return "Book is added successfully";
}
return "We are not able to add a book";
}

In the request body, the author details are sent. But we are saving author id in our Book table. So first we have to get the author’s id from author details and then pass the authorId as params.

Create Controller

Next, let us generate a BookController. For that run the below command to create a new controller in app/Http/Controllers.

php artisan make:controller BookController

You will find a new file named BookController.php in the app\Http\Controllers directory.

Create the constructor in the Controller layer(BookController) and pass the service layer(BookService), as a constructor parameter.

private $bookService;

public function __construct(BookService $bookService)
{
$this->bookService = $bookService;
}

Let us write our logic in the addBook() method in the controller where we will be sending the request body as parameters for the service layer(BookService.php) and returning the response as JSON.

public function addBook(Request $request): JsonResponse
{
$bookAdded = $this->bookService->addBook($request->all()["title"], $request->all()["price"], $request->all()["author"]);
return response()->json($bookAdded, 201);
}

Verifying the Endpoints

Before testing, make sure your application is running. You can use the inbuilt command as mentioned earlier:

php artisan serve

To test this endpoint open Postman and make a POST request to http://127.0.0.1:8000/books

Add books

Implementing GET, Update and Delete endpoints

We can implement these by using a similar approach as we did for the AddBook API.

app/Repository/IBookRepository.php

function getBookDetails($bookId);

function deleteBook($bookId): int;

function updateBook($bookId,$title , $price): int;

app/Repository/BookRepository.php

public function getBookDetails($bookId)
{
return Books::query()->where('books.id', '=', $bookId)->first();
}

public function deleteBook($bookId): int
{
return Books::query()->where('books.id', '=', $bookId)->delete();
}
public function updateBook($bookId,$title, $price): int
{
return DB::table('books')->where('books.id', '=', $bookId)->update(['books.title' => $title,'books.price' => $price]);
}

app/Repository/IAuthorRepository.php

function getAuthorDetailsFromAuthorId($authorId);

app/Repository/AuthorRepository.php

public function getAuthorDetailsFromAuthorId($authorId)
{
return Authors::query()->where('authors.id', '=', $authorId)->first(['first_name','last_name','email']);
}

app/Service/BookService.php

public function getBookDetails($bookId): array
{
$bookDetails = $this->bookRepository->getBookDetails($bookId);
if ($bookDetails == null) {
return [];
}
$authorDetails = $this->authorRepository->getAuthorDetailsFromAuthorId($bookDetails->author_id);


return array("title" => $bookDetails->title, "price" => (int)$bookDetails->price, "author" => $authorDetails);

}
public function deleteBook($bookId): string
{
$isDeleted = $this->bookRepository->deleteBook($bookId);
if ($isDeleted == 1) {
return "Book is deleted successfully";
}
return "Book is not present";
}
public function updateBook($bookId, $title, $price): string
{
if ($this->bookRepository->updateBook($bookId, $title, $price) == 1) {
return "Book is updated successfully";
}
return "Book is not available";
}

app/Http/Controllers/BookController.php

public function getBook($bookId): JsonResponse
{
$bookDetails = $this->bookService->getBookDetails($bookId);
return response()->json($bookDetails);
}
public function deleteBook($bookId): JsonResponse
{
$isDeleted = $this->bookService->deleteBook($bookId);
return response()->json($isDeleted);
}
public function updateBook($bookId, Request $request): JsonResponse
{
$bookUpdated = $this->bookService->updateBook($bookId ,$request->all()["title"] , $request->all()["price"]);
return response()->json($bookUpdated);
}

Verifying the Endpoints

GetBook details

Get book details

UpdateBook details

Update book details

DeleteBook

Delete book

With that, we have successfully built the REST APIs for our hypothetical BookShop application. In the subsequent instalments of this series, we will be looking into testing and documenting them.

Part II- Testing

Part III- Documenting

--

--