Back to blog

Creating APIs for Mobile Apps Using Laravel - Part II

Jul 10, 2024
.
Kristína Odziomková

In the previous part of this blog series, we demonstrated how to quickly scaffold an API in Laravel with authentication via API tokens and examples of login, logout and user detail endpoints, both in REST and GraphQL.

In this part, we will dive deeper into the differences between REST and GraphQL API and craft more, business logic-oriented, endpoints.


How do we decide when to use REST and when to use GraphQL for an API?

The biggest selling point for us is this:

"With REST APIs, endpoints are fixed and return predefined data structures. This can lead to over-fetching (receiving more data than needed) or under-fetching (not getting enough data in a single request). GraphQL allows clients to specify exactly what data they need, avoiding these issues by enabling clients to request only the fields they require, reducing network overhead and improving performance."

Therefore we use GraphQL for APIs in all the projects, whenever possible.

Unless:

  1. we are preparing an API that a 3rd party will consume - they usually prefer REST APIs
  2. the mobile app will be consuming data from multiple APIs, most of them REST - we don’t want to maintain both the REST and GraphQL API setup in the mobile app, if possible.

For GraphQL in Laravel we recommend this Composer package: ​​Lighthouse

Of course GraphQL has some cons over REST API, such as:

  1. Caching: Unlike REST, GraphQL lacks built-in support for caching.
  2. Over-fetching and Under-fetching: Despite allowing clients to specify the exact data they need, poorly constructed queries in GraphQL can result in over-fetching (receiving more data than needed) or under-fetching (not obtaining all the required data), potentially affecting performance and efficiency.
  3. Complexity in Nested Data: The flexibility of GraphQL in fetching nested data may introduce complexities, especially when handling deeply nested structures, making query optimization for performance more challenging.

In the end, it is about finding the best compromise for your use case.


As a sample project for demonstration purposes, we have chosen to implement an event management system. The administrator has the capability to create, update, delete, and publish events (such as art exhibitions, concerts, etc.) using the admin panel. The mobile app client can query them and sign-up/sign-out of individual events.

The admin panel implementation with examples will be described in the next part of this blog series. In this part, we will look into preparing some basic API endpoints for the mobile app client.

Quick scaffold of the events model (15 minutes)

1. The first step is to create migration for the models.

Event attributes:

EventCategory attributes:

EventParticipant attributes:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('event_categories', function (Blueprint $table) {
            $table->id();
            
            $table->jsonb('title');
            
            $table->timestamps();
        });
        
        Schema::create('events', function (Blueprint $table) {
            $table->id();
            
            $table->jsonb('title');
            $table->jsonb('description')->nullable();
            $table->integer('max_number_of_participants')->nullable();
            $table->foreignId('event_category_id')->constrained('event_categories');
            $table->dateTime('date_time');

            $table->timestamps();
            $table->softDeletes();
        });
        
        Schema::create('event_participants', function (Blueprint $table) {
            $table->id();

            $table->foreignId('mobile_app_user_id')->constrained('mobile_app_users');
            $table->foreignId('event_id')->constrained('events');

            $table->timestamps();
        });
    }
};

2. Run migrations

sail artisan migrate

Create Eloquent models, controllers, seeders, factories, form requests, API resources and collections…

sail artisan make:model EventCategory -fsc --requests
 sail artisan make:model Event -fsc --requests
 sail artisan make:resource EventResource
 sail artisan make:resource EventCategoryResource
 sail artisan make:resource EventCollection
 sail artisan make:resource EventCategoryCollection

-fsc creates factory, seeder, controller

If these command options are not supported for your version of Laravel, create the necessary files manually.

Now you should have this structure:

app/Models/EventCategory.php
app/Models/Event.php
database/factories/EventCategoryFactory.php
database/factories/EventFactory.php
database/seeders/EventCategorySeeder.php
database/seeders/EventSeeder.php
app/Http/Requests/StoreEventCategoryRequest.php
app/Http/Requests/UpdateEventCategoryRequest.php
app/Http/Requests/StoreEventRequest.php
app/Http/Requests/UpdateEventRequest.php
app/Http/Controllers/EventCategoryController.php
app/Http/Controllers/EventController.php
app/Http/Resources/EventCollection.php
app/Http/Resources/EventCategoryCollection.php
app/Http/Resources/EventResource.php
app/Http/Resources/EventCategoryResource.php

Filled in models:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class EventCategory extends Model
{
    use HasFactory;

    protected $guarded = ['id'];
    
    protected $casts = [
        'title' => 'array',
    ];
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Event extends Model
{
    use HasFactory;

    protected $guarded = ['id'];
    
    protected $casts = [
        'title' => 'array',
        'description' => 'array',
    ];

    public function category(): BelongsTo
    {
        return $this->belongsTo(EventCategory::class, 'event_category_id');
    }
    
     public function participants(): BelongsToMany
    {
        return $this->belongsToMany(
            MobileAppUser::class, 
            'event_participants', 
            'event_id', 
            'mobile_app_user_id'
        )->withTimestamps();
    }
}

Factories:

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\EventCategory>
 */
class EventCategoryFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition(): array
    {
        return [
            'title' => [
                'en' => $this->faker->word(),
                'es' => $this->faker->word(),
            ]
        ];
    }
}
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event>
 */
class EventFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition(): array
    {
        return [
            'title' => [
                'en' => $this->faker->words(2),
                'es' => $this->faker->words(2),
            ],
            'description' => [
                'en' => $this->faker->paragraph(),
                'es' => $this->faker->paragraph(),
            ],
            'max_number_of_participants' => $this->faker->numberBetween(1, 20),
            'date_time' => $this->faker->dateTimeBetween('now', '+1 year'),
        ];
    }
}

Seeders:

<?php

namespace Database\Seeders;

use App\Models\EventCategory;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class EventCategorySeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        EventCategory::factory()->count(5)->create();
    }
}
<?php

namespace Database\Seeders;

use App\Models\Event;
use App\Models\EventCategory;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class EventSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Event::factory()->count(5)->create([
            'event_category_id' => EventCategory::all()->random()->id,
        ]);
        Event::factory()->count(5)->create([
            'event_category_id' => EventCategory::all()->random()->id,
        ]);
        Event::factory()->count(5)->create([
            'event_category_id' => EventCategory::all()->random()->id,
        ]);
    }
}

To seed event categories and events, run:

sail composer dump-autoload
sail artisan db:seed --class=EventCategorySeeder
sail artisan db:seed --class=EventSeeder

In the next section we will fill out the routes, controllers, form requests and API resources while implementing the API endpoints.


GraphQL/REST API for events (1 - 2 hours)

In this section, we will create endpoints for querying paginated event listings and accessing detailed information about a single event. Additionally, we will implement functionality for signing up to and signing out of events. These implementations will be demonstrated using both REST and GraphQL for illustration.

If you followed the part I. of this blog series, you should already have a routes/api.php file (for REST API) and/or graphql/schema.graphql file (for GraphQL API) and authentication implementation.

REST API

1. Querying data

routes/api.php

Route::middleware('auth:mobile-app')->group(function () {
    Route::get('event-categories', 'App\Http\Controllers\EventCategoryController@get')->name('event-category.get');
    Route::get('events', 'App\Http\Controllers\EventController@paginated')->name('events.paginated');
    Route::get('events/{event}', 'App\Http\Controllers\EventController@detail')->name('events.detail');
});

App\Http\Controllers\EventCategoryController.php

<?php
namespace App\Http\Controllers;
use App\Http\Resources\EventCategoryCollection;
use App\Models\EventCategory;
class EventCategoryController extends Controller
{
    public function get(): EventCategoryCollection
    {
        return new EventCategoryCollection(EventCategory::all());
    }
}

App\Http\Resources\EventCategoryCollection.php

<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class EventCategoryCollection extends ResourceCollection
{
    public $collects = EventCategoryResource::class;
}

App\Http\Resources\EventCategoryResource.php

<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class EventCategoryResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
        ];
    }
}

Postman examples

Don’t forget to send the Accept: application/json header, along with the API token as Bearer Token type of authorization

GET event categories
GET event categories

App\Http\Controllers\EventController.php

<?php
namespace App\Http\Controllers;
use App\Http\Resources\EventCollection;
use App\Http\Resources\EventResource;
use App\Models\Event;
use Illuminate\Http\Request;
class EventController extends Controller
{
    public function get(): EventCategoryCollection
    {
        return new EventCategoryCollection(EventCategory::all());
    }
    public function detail(Request $request, Event $event): EventResource
    {
        return new EventResource($event);
    }
}

App\Http\Resources\EventCollection.php

<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class EventCollection extends ResourceCollection
{
    public $collects = EventResource::class;
}

App\Http\Resources\EventResource.php

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class EventResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'description' => $this->description,
            'max_number_of_participants' => $this->max_number_of_participants,
            'event_category' => new EventCategoryResource($this->eventCategory),
            'date_time' => $this->date_time
        ];
    }
}

Postman examples

Don’t forget to send the Accept: application/json header, along with the API token as Bearer Token type of authorization

GET paginated events
GET paginated events
GET event by ID
GET event by ID

2. Mutating data

routes/api.php

Route::middleware('auth:mobile-app')->group(function () {
    Route::post('events/{event}/signup', 'App\Http\Controllers\EventController@signup')->name('events.signup');
    Route::post('events/{event}/signout', 'App\Http\Controllers\EventController@signout')->name('events.signout'); 
});

App\Http\Controllers\EventController.php

<?php

namespace App\Http\Controllers;

use App\Http\Resources\EventCategoryCollection;
use App\Models\EventCategory;

class EventCategoryController extends Controller
{
    ...
    
    public function signup(Request $request, Event $event): JsonResponse
    {
        $user = Auth::guard('mobile-app')->user();

        if($event->participants()->where('mobile_app_users.id', $user->id)->exists()) {
            return response()->json(['message' => 'Already participated'], 400);
        }

        if($event->participants()->count() >= $event->max_number_of_participants) {
            return response()->json(['message' => 'Max. number of participants reached'], 400);
        }

        $event->participants()->attach($user->id);

        return response()->json(new EventResource($event));
    }

    public function signout(Request $request, Event $event): JsonResponse
    {
        $user = Auth::guard('mobile-app')->user();

        $event->participants()->detach($user->id);

        return response()->json(new EventResource($event));
    }
}

Postman examples

Don’t forget to send the Accept: application/json header, along with the API token as Bearer Token type of authorization.