Simple way to create an api service with Laravel
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)
Please login to leave a comment.