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)
Comments
Great Tools for Developers
Git tower
A powerful Git client for Mac and Windows that simplifies version control.
Mailcoach's
Self-hosted email marketing platform for sending newsletters and automated emails.
Uptimia
Website monitoring and performance testing tool to ensure your site is always up and running.
Please login to leave a comment.