How to Build a REST API with Laravel 5.0 (2015)
Short answer: Define routes in app/Http/routes.php, create API controllers with php artisan make:controller, use Eloquent's toArray() for serialization, and return response()->json($data, $statusCode). Laravel 5 added middleware groups — put API auth in api middleware group, not web.
Install Laravel 5.0
composer create-project laravel/laravel myapi 5.0.*
cd myapi
php artisan --version # Laravel Framework version 5.0.x
API Routes
<?php
// app/Http/routes.php
// All API routes prefixed with /api/v1
Route::group(['prefix' => 'api/v1', 'middleware' => 'api.auth'], function() {
// Products
Route::get ('products', 'Api\ProductController@index');
Route::get ('products/{id}', 'Api\ProductController@show');
Route::post ('products', 'Api\ProductController@store');
Route::put ('products/{id}', 'Api\ProductController@update');
Route::delete('products/{id}', 'Api\ProductController@destroy');
// Nested: product reviews
Route::get ('products/{id}/reviews', 'Api\ReviewController@index');
Route::post ('products/{id}/reviews', 'Api\ReviewController@store');
// Orders
Route::get ('orders', 'Api\OrderController@index');
Route::post ('orders', 'Api\OrderController@store');
Route::get ('orders/{id}', 'Api\OrderController@show');
});
// Auth (no middleware required)
Route::post('api/v1/auth/login', 'Api\AuthController@login');
Route::post('api/v1/auth/register', 'Api\AuthController@register');
API Base Controller
<?php
// app/Http/Controllers/Api/ApiController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
class ApiController extends Controller {
protected function success(mixed $data, int $code = 200): JsonResponse {
return response()->json([
'success' => true,
'data' => $data,
], $code);
}
protected function error(string $message, int $code = 400, array $errors = []): JsonResponse {
$payload = [
'success' => false,
'message' => $message,
];
if (!empty($errors)) {
$payload['errors'] = $errors;
}
return response()->json($payload, $code);
}
protected function paginated($collection, $paginator): JsonResponse {
return response()->json([
'success' => true,
'data' => $collection,
'meta' => [
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
],
]);
}
}
Product Controller
<?php
// app/Http/Controllers/Api/ProductController.php
namespace App\Http\Controllers\Api;
use App\Models\Product;
use App\Http\Requests\Api\StoreProductRequest;
use App\Http\Requests\Api\UpdateProductRequest;
class ProductController extends ApiController {
public function index(): JsonResponse {
$products = Product::with('category')
->active()
->orderBy('created_at', 'desc')
->paginate(20);
return $this->paginated(
$products->items(),
$products
);
}
public function show(int $id): JsonResponse {
$product = Product::with(['category', 'images', 'reviews'])
->findOrFail($id);
return $this->success($product->toApiArray());
}
public function store(StoreProductRequest $request): JsonResponse {
// Form Request handles validation — see below
$product = Product::create($request->validated());
return $this->success($product->toApiArray(), 201);
}
public function update(UpdateProductRequest $request, int $id): JsonResponse {
$product = Product::findOrFail($id);
$product->update($request->validated());
return $this->success($product->fresh()->toApiArray());
}
public function destroy(int $id): JsonResponse {
$product = Product::findOrFail($id);
$product->delete();
return $this->success(null, 204);
}
}
Form Request Validation (Laravel 5.0 Feature)
<?php
// app/Http/Requests/Api/StoreProductRequest.php
// Laravel 5.0's Form Requests — replaces manual validation in controller
namespace App\Http\Requests\Api;
use App\Http\Requests\Request;
class StoreProductRequest extends Request {
public function authorize(): bool {
// Check if the authenticated user can create products
return $this->user()->hasRole('admin') || $this->user()->hasRole('seller');
}
public function rules(): array {
return [
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'stock' => 'required|integer|min:0',
'category_id' => 'required|exists:categories,id',
'description' => 'nullable|string|max:5000',
'sku' => 'required|unique:products,sku|alpha_dash',
];
}
public function messages(): array {
return [
'sku.unique' => 'A product with this SKU already exists.',
'category_id.exists' => 'The selected category does not exist.',
];
}
}
Eloquent Model with API Serialization
<?php
// app/Models/Product.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model {
protected $fillable = ['name', 'price', 'stock', 'category_id', 'description', 'sku', 'status'];
protected $casts = [
'price' => 'float',
'stock' => 'integer',
'is_active' => 'boolean',
];
// Scope for active products
public function scopeActive($query) {
return $query->where('status', 'active')->where('stock', '>', 0);
}
// Relationships
public function category() {
return $this->belongsTo(Category::class);
}
public function images() {
return $this->hasMany(ProductImage::class)->orderBy('sort_order');
}
// Custom API serialization — control exactly what goes to client
public function toApiArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
'sku' => $this->sku,
'price' => $this->price,
'price_formatted' => number_format($this->price, 2) . ' USD',
'stock' => $this->stock,
'in_stock' => $this->stock > 0,
'category' => $this->category ? [
'id' => $this->category->id,
'name' => $this->category->name,
] : null,
'images' => $this->images->map(fn($img) => $img->url)->toArray(),
'created_at' => $this->created_at->toIso8601String(),
];
}
}
API Authentication Middleware
<?php
// app/Http/Middleware/ApiAuthenticate.php
namespace App\Http\Middleware;
use Closure;
use App\Models\ApiToken;
class ApiAuthenticate {
public function handle($request, Closure $next) {
$token = $request->header('Authorization');
if (!$token || !str_starts_with($token, 'Bearer ')) {
return response()->json(['success' => false, 'message' => 'Unauthenticated'], 401);
}
$tokenString = substr($token, 7);
$apiToken = ApiToken::where('token', hash('sha256', $tokenString))
->where('expires_at', '>', now())
->with('user')
->first();
if (!$apiToken) {
return response()->json(['success' => false, 'message' => 'Invalid or expired token'], 401);
}
// Attach user to request for use in controllers
$request->setUserResolver(fn() => $apiToken->user);
$apiToken->touch(); // Update last_used_at
return $next($request);
}
}
Error Handling (app/Exceptions/Handler.php)
<?php
// Override render() for consistent JSON errors
public function render($request, Exception $e) {
// Only handle API routes
if ($request->is('api/*')) {
if ($e instanceof ModelNotFoundException) {
return response()->json([
'success' => false,
'message' => 'Resource not found',
], 404);
}
if ($e instanceof ValidationException) {
return response()->json([
'success' => false,
'message' => 'Validation failed',
'errors' => $e->errors(),
], 422);
}
if ($e instanceof AuthorizationException) {
return response()->json([
'success' => false,
'message' => 'Forbidden',
], 403);
}
if (config('app.debug')) {
return response()->json([
'success' => false,
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
], 500);
}
return response()->json(['success' => false, 'message' => 'Server error'], 500);
}
return parent::render($request, $e);
}
Testing the API
# Create product
curl -X POST https://api.example.com/api/v1/products \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{"name":"Widget","sku":"WGT-001","price":29.99,"stock":100,"category_id":3}'
# Response
{
"success": true,
"data": {
"id": 42,
"name": "Widget",
"sku": "WGT-001",
"price": 29.99,
"price_formatted": "29.99 USD",
"stock": 100,
"in_stock": true,
"category": {"id": 3, "name": "Widgets"},
"images": [],
"created_at": "2015-04-05T14:32:00+00:00"
}
}
Laravel 5's Form Requests eliminated the validation boilerplate that cluttered CodeIgniter controllers. The resource controller pattern (index/show/store/update/destroy) became the standard API structure that's still used in 2025.