AboutBlogContact
BackendApril 5, 2015 5 min read 26

How to Build a REST API with Laravel 5.0 (2015)

AunimedaAunimeda
📋 Table of Contents

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.

Read Also

GraphQL vs REST API in 2026: A Practical Guide to Choosing the Right Approachaunimeda
Backend

GraphQL vs REST API in 2026: A Practical Guide to Choosing the Right Approach

GraphQL has been 'the future of APIs' for almost a decade. REST has been 'dying' for just as long. Both are still widely used in 2026 — because they solve different problems. Here's a practical framework for choosing, with real tradeoffs from production systems.

How to Implement Real-Time Features with WebSockets and Socket.io 1.x (2016)aunimeda
Backend

How to Implement Real-Time Features with WebSockets and Socket.io 1.x (2016)

Socket.io 1.x (2016) made WebSockets practical: automatic fallback to polling, rooms for namespacing, and a simple emit/on API. We used it to build a real-time order tracking dashboard. Here's the server setup, client integration, authentication via JWT, and scaling with Redis adapter.

How to Add Full-Text Search to Your App with Elasticsearch 2.x (2016)aunimeda
Backend

How to Add Full-Text Search to Your App with Elasticsearch 2.x (2016)

MySQL LIKE queries break at scale. When our product catalog reached 200k items, search took 4+ seconds. Elasticsearch 2.x solved it: 50ms search across 200k documents with relevance scoring, typo tolerance, and faceted filters. Here's the indexing strategy, mapping, and PHP/Node.js integration.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Get Consultation All articles