Fermin Perdomo

Senior Full Stack Engineer | PHP | JavaScript

Simple way to create an api service with Laravel

Fermin Perdomo
September 26, 2025

1) New project + API mode

composer create-project laravel/laravel thirdparty-api
cd thirdparty-api
php artisan serve

In .env set your DB and APP_URL.


2) Auth for third parties (Sanctum personal tokens)

composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Enable Sanctum on API routes (token guard is default in Laravel 12).

Create a token (admin-only action example):

// Somewhere in your admin controller or tinker
$user = \App\Models\User::first();
$token = $user->createToken('partner-foo')->plainTextToken;
// Give this token to the partner (store securely).

Partners send:

Authorization: Bearer <token>

3) Versioned routes + rate limit

routes/api.php

use App\Http\Controllers\Api\V1\ContactController;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Cache\RateLimiting\Limit;

RateLimiter::for('partner', fn($request) => [
    Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip()),
]);

Route::prefix('v1')
    ->middleware(['auth:sanctum','throttle:partner'])
    ->group(function () {
        Route::get('contacts', [ContactController::class, 'index']);
        Route::post('contacts', [ContactController::class, 'store']);
        Route::get('contacts/{contact}', [ContactController::class, 'show']);
        Route::patch('contacts/{contact}', [ContactController::class, 'update']);
        Route::delete('contacts/{contact}', [ContactController::class, 'destroy']);
    });

4) Model + migration

php artisan make:model Contact -m

database/migrations/xxxx_create_contacts_table.php

public function up(): void
{
    Schema::create('contacts', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email')->unique();
        $table->string('phone')->nullable();
        $table->timestamps();
    });
}

php artisan migrate

5) Requests (validation) + Resource (serialization)

php artisan make:request Api/V1/StoreContactRequest
php artisan make:request Api/V1/UpdateContactRequest
php artisan make:resource Api/V1/ContactResource

app/Http/Requests/Api/V1/StoreContactRequest.php

class StoreContactRequest extends FormRequest
{
    public function authorize(): bool { return true; }

    public function rules(): array
    {
        return [
            'name'  => ['required','string','max:120'],
            'email' => ['required','email','max:255','unique:contacts,email'],
            'phone' => ['nullable','string','max:40'],
        ];
    }
}

app/Http/Requests/Api/V1/UpdateContactRequest.php

class UpdateContactRequest extends FormRequest
{
    public function authorize(): bool { return true; }

    public function rules(): array
    {
        $id = $this->route('contact')->id;
        return [
            'name'  => ['sometimes','required','string','max:120'],
            'email' => ['sometimes','required','email','max:255',"unique:contacts,email,{$id}"],
            'phone' => ['nullable','string','max:40'],
        ];
    }
}

app/Http/Resources/Api/V1/ContactResource.php

class ContactResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'    => $this->id,
            'name'  => $this->name,
            'email' => $this->email,
            'phone' => $this->phone,
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }
}

6) Controller (clean CRUD, pagination, filters)

php artisan make:controller Api/V1/ContactController

app/Http/Controllers/Api/V1/ContactController.php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1\StoreContactRequest;
use App\Http\Requests\Api\V1\UpdateContactRequest;
use App\Http\Resources\Api\V1\ContactResource;
use App\Models\Contact;
use Illuminate\Http\Request;

class ContactController extends Controller
{
    public function index(Request $request)
    {
        $query = Contact::query();

        // Simple filtering
        if ($email = $request->query('email')) {
            $query->where('email', $email);
        }
        if ($search = $request->query('q')) {
            $query->where(function ($q) use ($search) {
                $q->where('name', 'like', "%{$search}%")
                  ->orWhere('email', 'like', "%{$search}%");
            });
        }

        $contacts = $query->orderByDesc('id')->paginate(
            perPage: min((int) $request->query('per_page', 20), 100)
        );

        return ContactResource::collection($contacts)
            ->additional(['meta' => ['version' => '1.0']]);
    }

    public function store(StoreContactRequest $request)
    {
        $contact = Contact::create($request->validated());
        return (new ContactResource($contact))
            ->response()
            ->setStatusCode(201);
    }

    public function show(Contact $contact)
    {
        return new ContactResource($contact);
    }

    public function update(UpdateContactRequest $request, Contact $contact)
    {
        $contact->update($request->validated());
        return new ContactResource($contact);
    }

    public function destroy(Contact $contact)
    {
        $contact->delete();
        return response()->noContent();
    }
}

7) Consistent error shape (optional, but recommended)

In app/Exceptions/Handler.php, ensure validation errors and other exceptions return JSON for API routes (Laravel 12 already does for JSON requests). You can standardize further with a small formatter if needed.

8) CORS (so partners can call from browsers)

Install & configure if you need custom rules:

  • Edit config/cors.php (allowed_origins, paths => ['api/*']).
  • In .env, set SESSION_DRIVER=cookie is fine; Sanctum personal tokens work from servers and browsers.

9) API keys alternative (HMAC or header key)

If you don’t want user accounts, add a lightweight API-Key middleware:

php artisan make:middleware ApiKeyGuard

app/Http/Middleware/ApiKeyGuard.php

class ApiKeyGuard
{
    public function handle($request, Closure $next)
    {
        $provided = $request->header('X-Api-Key');
        $valid = config('services.partners.keys', []); // e.g., ['foo' => 'abc123', 'bar' => 'xyz456']

        if (! $provided || ! in_array($provided, $valid, true)) {
            return response()->json(['message' => 'Unauthorized'], 401);
        }

        return $next($request);
    }
}

Register in app/Http/Kernel.php, then swap auth:sanctum for apikey in routes if desired.

10) Docs (OpenAPI)

composer require "darkaonline/l5-swagger"
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"

Add annotations to controllers/requests, then:

php artisan l5-swagger:generate

Docs available at /api/documentation (configurable). Share with partners.

11) Testing (Pest)

composer require pestphp/pest --dev
php artisan pest:install
php artisan make:test Api/V1/ContactApiTest

tests/Feature/Api/V1/ContactApiTest.php

it('lists contacts', function () {
    $user = \App\Models\User::factory()->create();
    \App\Models\Contact::factory()->count(3)->create();

    $this->actingAs($user, 'sanctum')
        ->getJson('/api/v1/contacts')
        ->assertOk()
        ->assertJsonStructure(['data', 'links', 'meta']);
});

12) Usage examples

List (with search & pagination):

curl -H "Authorization: Bearer <TOKEN>" \
  "https://your-app.test/api/v1/contacts?q=fermin&per_page=10"

Create:

curl -X POST -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
  -d '{"name":"Hamlet","email":"[email protected]","phone":"+1-809-555-0000"}' \
  https://your-app.test/api/v1/contacts

Extras you can add later

  • Webhooks (signed with HMAC header)
  • Request signing (HMAC of body + timestamp to prevent replay)
  • Soft deletes with SoftDeletes
  • Cursor pagination for very large datasets
  • E-tags/If-None-Match for caching
  • Feature flags per partner (limits, fields)


Reactions

Loading reactions...
Log in to react to this post.

Comments

Please login to leave a comment.

Newsletter