Simple way to create an api service with Laravel
Fermin Perdomo
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)
Newsletter
Get new posts delivered straight to your inbox.
Great Tools for Developers
Git Tower
Get Started - It's FreeA powerful Git client for Mac and Windows that simplifies version control.
Mailcoach
Start freeSelf-hosted email marketing platform for sending newsletters and automated emails.
Uptimia
Start freeWebsite monitoring and performance testing tool to ensure your site is always up and running.
Cloudways
Start freeManaged cloud hosting platform that simplifies server management for developers.
Comments
No comments yet. Be the first to share your thoughts.