AboutBlogContact
Backend EngineeringApril 5, 2015 5 min read 145Updated: June 22, 2026

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

AunimedaAunimeda
📋 Table of Contents

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.


Aunimeda builds production-grade backend systems - APIs, microservices, real-time applications, and system integrations.

Contact us for backend engineering services. See also: Custom Software Development, Web Development

Read Also

Node.js + TypeScript: Building a Production REST API from Scratch in 2026aunimeda
Backend Engineering

Node.js + TypeScript: Building a Production REST API from Scratch in 2026

A complete guide to building a production-ready REST API with Node.js and TypeScript - authentication, validation, error handling, rate limiting, logging, and deployment. No shortcuts.

PostgreSQL Performance Optimization: The Practical Guide for 2026aunimeda
Backend Engineering

PostgreSQL Performance Optimization: The Practical Guide for 2026

Slow queries, missing indexes, N+1 problems, and connection pool exhaustion account for 90% of PostgreSQL performance issues. Here's how to diagnose and fix each one with real queries.

Redis Data Structures in Production: Beyond SET and GETaunimeda
Backend Engineering

Redis Data Structures in Production: Beyond SET and GET

Most teams use Redis as a glorified hash map. This guide covers the data structures that solve real production problems - sorted sets for leaderboards, streams for durable event queues, HyperLogLog for UV counting at scale, and Lua scripts for atomic operations you can't otherwise do safely.

Need IT development for your business?

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

Get Consultation All articles