Lumen API 03: Creating Route To Show One Item

Yanuar Arifin
7 min readOct 15, 2019
Photo by timothy muza on Unsplash

Previously, we have tried how to create a route and process it inside the routes/web.php file. Now we will make a route, then handle it in a controller, as this is common way people do; the infamous MVC. Though we will not render a view but return JSON.

This is the step that we need to creating an index route:

  1. Create Route, link it to a controller function.
  2. Capture the request.
  3. Query data from database.
  4. Return a response to requester.

What This Route Do?

This route will show one item from database. When you make a new function on your codebase, you would think about all those cases and possibilities how this function would be used and what should be the response. Especially in the design phase, we will talk about how the function behaves, Let’s write those down so we could remember. This is the list of behaviors that I can think of:

  1. When we try to get the data that exists, it should gave the correct data from database by id.
  2. when there’s no data, it should return 404.

What if I tell you we could make the computer check for those? there’s no need for us to test those manually, just fire the command away. To do that, open the tests/ItemFeatureTest.php and add these functions :

public function testItemShowsCorrectDbRecord()
{
factory(App\Item::class, 5)->create();
$this->get("/items/4"); $response = $this->response->getContent(); $response = json_decode($response, true); $this->seeInDatabase('items', $response['data']['attributes']);
}
public function testGetANonExistentItemReturns404()
{
$this->get("/items/8")
->assertResponseStatus(404);
}

Now, run it. Yes, run it even if we haven’t make any code.

Test Result before even make any code

So you see, the testGetNonExistentItemReturns404() test immediately works. Wow! how could that be? Turns out that lumen already make 404 response when you go to nonexistent page. And the other one is failing, that’s because there are no response, next step we will write the code to fix that.

By the way, if you notice in the picture above, I use --testdox argument when running the test, it will show the test name that is being run, and give the summary of of the error on non successful test. This is the difference when you do not use the argument

without testdox argument vs with argument

Why we do test first without writing any code?

It’s the TDD mantra, if you do not know what TDD is, it is a coding methodology where you make test first, then move forward to make the code passes the test, when all the test is passed, we could start to refactor our code to make it better. This steps is called red green refactor, which is what TDD is being done.

Red — Geen — Refactor

— TDD Mantra

If you still need more convincing, I have made this article about why we need to make a test based on my experience.

Do We Need To Test This App?

Okay, let’s write the code now.

Point The Route To a Controller Function

Let’s start by creating a new controller to accept the request . Create a new file called ItemController.php in app/Http/Controllers that extends Controller in the same directory. Inside the new controller, we make a query to database, then make a response.

<?php
# app/Http/Controllers/ItemController.php
namespace App\Http\Controllers;class ItemController extends Controller
{
public function showOne()
{
return response()->json(['hey' => 'there']);
}
}
?>

opening the routes/web.php file. We will modify the route that we have made before from using closure (the function that is used as second argument) to pointing it to a controller function. Change this code:

$router->get('/items', ["as" => "item.showOne", "uses" => "ItemController@showOne");

This change will tell Lumen instead of running the function to get the response, it should seek ItemController in app/Http/Controllers directory and run showOne() function to process the request. So make sure that you put the controller in the correct directory and using the right namespace (the fist line after php tag).

This is roughly how the routes/web.php now:

<?php/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It is a breeze. Simply tell Lumen the URIs it should respond to
| and give it the Closure to call when that URI is requested.
|
*/
$router->get('/', function () use ($router) {
return $router->app->version();
});
$router->get('/items', "ItemController@showAll");$router->get('/items', ["as" => "item.showOne", "uses" => "ItemController@showOne");

Let’s fill the code inside the ItemController@showOne

But Before That, API Response Has Standard

$router->get('/items', "ItemController@showAll");

Now to check whether the new route modification is working go the terminal, navigate to your project root, and executeBut Before That, API Response Has Standard

The nature of an API is a different that a website that shows page to visitor. Website returns HTML for web browser to render and human to see while the purpose of an APIs is to make other computer program to be able to use it, be it a front end framework, other API, or you sell the API as a service to clients.

Why an API needs standard? because when you serve multiple client, the crucial thing is that you do not give your client headache because they could not predict your API. We use computer to automate task so we could do fun things, if your API responses changing like day and night cycle, then you waste your client’s time by making them adjust their API handler code to align with your modified API instead of doing other things that more valuable to them.

Website returns HTML for web browser to render and human to see while the purpose of an APIs is to make other computer program

As stated in previous article(the first one), I will use jsonapi.org standard as reference, It’s fairly simple to use. Basically you turn the model attribute to JSON, then wrap it in some other information. Do not worry, just read the jsonapi example and documentation to understand how the standard is about, then we will use Resource class from Lumen to quickly create the schema.

Another important thing that I wanted to mention is some resources about API guidelines, there are many guidelines that you could read to have a better understanding from experienced people/company, such as:

Or you could search for books and videos. Let’s continue our app for now.

Create Resource Class

This class will transform our model to the JSON representation that we want. I will paste the example json from jsonapi.org here for comparison, and then we work on our code to make it equal to this example.

{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "Rails is Omakase"
},
"links": {
"self": "http://example.com/articles/1"
}
}

This is the example that you could find in https://jsonapi.org/format/ (there’s relationships key there, but I omit because we do not have that yet). As you could see above, the response will be the Model, wrapped inside attributes key, and added by type , id , and links to that resource(model).

Let’s create the resource class for that. Make new file Item.php on app/Http/Resources directory, if Resources directory is not exists, create it. We will make a resource class of item to convert our model to JSON format that is equal with the jsonapi standard above. Start by extending Json\Resource class, then fill the format inside toArray() method.

<?php# app/Http/Resources/Item.phpnamespace App\Http\Resources;use Illuminate\Http\Resources\Json\JsonResource;class Item extends JsonResource
{
/**
* Turns Model to json response
*
* @return void
*/
public function toArray($request)
{
return [
'type' => 'item',
'id' => $this->id,
'attributes' => [
'sku' => $this->sku,
'name' => $this->name,
'description' => $this->description,
'price' => $this->price,
'stock' => $this->stock,
'updated_at' => $this->updated_at,
'created_at' => $this->created_at
],
'links' => [
"self" => route('item.showOne', ['id' => $this->id])
]
];
}

}
?>

Next we will use this class to make a response on the controller. Open your app/Http/ItemController and we will make the code on showOne() function.

<?phpnamespace App\Http\Controllers;use App\Http\Resources\Item as ItemResource;
use App\Item;
class ItemController extends Controller
{
public function showOne($id)
{
$item = Item::findOrFail($id);
return new ItemResource($item);
}
}

The first line on showOne() function is a model query, it will return the model if it exists, or it will make 404 exception if it does not exists. The next part is we use Item resource class to create the response, we could return the class directly without using response()->json() function.

This is the result if you try to access item.local/items/4 (the db has record with id 4)

result from get item.local/items/{id}

If you compare the response and the jsonapi format that I wrote above, they already have the same content, it has data object, inside it has type , id , attributes, and links . And the model’s attributes is inside attributes key. But if you look on app/Http/Resources/Item.php ‘s code, it does not have data attribute there, Lumen automatically wraps the returned array in data attribute, and if you already wrap the returned array with data , it will not make the outer data key double. You could disable the data too if you want. Check the documentation here:

Now Let’s run the the test that we previously create running the vendor/bin/phpartisan --testdox command

Test Result after Implementing Code

Alright, We are done with this feature. Commit your test, resource, and controller class.

Next we will use Resource collection to make a list of resource.

--

--

Yanuar Arifin

Software Engineer. Currently PHP. How far could we automate things?