Skip to the content.

Building a REST API with Laravel 11: A Complete Guide

This comprehensive guide walks through creating a RESTful API using Laravel 11, from initial setup to testing.

TLDR

All you need to know to build a REST API using Laravel 11.

Table of Contents

Prerequisites

Project Setup

1. Create New Laravel Project

composer create-project laravel/laravel test-api-jwt
cd test-api-jwt

2. Configure Environment

cp .env.example .env
php artisan key:generate
#this will generate a random encryption key for your application.

WAY 1: Update .env file for SQLite

DB_CONNECTION=sqlite

Create SQLite database:

touch database/database.sqlite

WAY 2: Update .env file for MySQL

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=test_api_jwt
DB_USERNAME=root
DB_PASSWORD=
# Set your MySQL password above

Creating the API

1. Edit app/Models/User.php Model

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasFactory, Notifiable, SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var list<string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        "user_level",
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var list<string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    public function products()
    {
      return $this->hasMany(Product::class);
    }
}

2. Edit database/migrations/0001_01_01_000000_create_users_table.php Migration

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->integer('user_level')->default(1);
            $table->softDeletes();
            $table->timestamps();
        });

        Schema::create('password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });

        Schema::create('sessions', function (Blueprint $table) {
            $table->string('id')->primary();
            $table->foreignId('user_id')->nullable()->index();
            $table->string('ip_address', 45)->nullable();
            $table->text('user_agent')->nullable();
            $table->longText('payload');
            $table->integer('last_activity')->index();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
        Schema::dropIfExists('password_reset_tokens');
        Schema::dropIfExists('sessions');
    }
};

3. Edit database/factories/UserFactory.php Factory

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
  /**
   * The current password being used by the factory.
   */
  protected static ?string $password;

  /**
   * Define the model's default state.
   *
   * @return array<string, mixed>
   */
  public function definition(): array
  {
    return [
      'name' => fake()->name(),
      'email' => fake()->unique()->safeEmail(),
      'email_verified_at' => now(),
      'password' => static::$password ??= Hash::make('password'),
      'remember_token' => Str::random(10),
      'user_level' => fake()->numberBetween(1, 3),
    ];
  }

  /**
   * Indicate that the model's email address should be unverified.
   */
  public function unverified(): static
  {
    return $this->state(fn(array $attributes) => [
      'email_verified_at' => null,
    ]);
  }
}

4. Create Database Seeders

php artisan make:seeder SuperAdminSeeder
php artisan make:seeder UserSeeder
<?php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;

class SuperAdminSeeder extends Seeder
{
    public function run(): void
    {
        User::factory()->create([
            'name' => 'Super Admin',
            'email' => 'sa@example.com',
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'user_level' => 0
        ]);
    }
};
<?php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;

class UserSeeder extends Seeder
{
  public function run(): void
  {
    User::factory()->count(15)->create();
  }
};

5. Create Product Model, Migration, Factory, Seeder, and Controller

php artisan make:model Product -mfsc --api
# -m for migration
# -f for factory
# -s for seeder
# -c for controller
# --api for API resources on controller

This creates:

6. Edit Product Model, Migration, Factory, Seeder, and Controller

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = [
        'user_id',
        'name',
        'price',
        'description',
        'stock'
    ];

    public function user()
    {
      return $this->belongsTo(User::class);
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained();
            $table->string('name');
            $table->text('description');
            $table->decimal('price', 10, 2);
            $table->integer('stock');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};
<?php

namespace Database\Factories;

use App\Models\Product;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
 */
class ProductFactory extends Factory
{
  /**
   * Define the model's default state.
   *
   * @return array<string, mixed>
   */
  public function definition(): array
  {
    return [
      'user_id' => User::inRandomOrder()->first()->id,
      'name' => fake()->unique()->words(3, true),
      'description' => fake()->paragraph(),
      'price' => fake()->randomFloat(2, 10, 1000),
      'stock' => fake()->numberBetween(0, 100),
      'created_at' => fake()->dateTimeBetween('-1 year'),
      'updated_at' => fake()->dateTimeBetween('-6 months'),
    ];
  }
}
<?php

namespace Database\Seeders;

use App\Models\Product;
use Faker\Factory;
use Illuminate\Database\Seeder;

class ProductSeeder extends Seeder
{
    public function run(): void
    {
        // Create 100 products using the factory
        Product::factory()->count(100)->create();
    }
};
<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Validator;

class ProductController extends Controller
{
  private array $rules = [
    'name' => 'required|string|max:255',
    'description' => 'required|string',
    'price' => 'required|numeric|min:0',
    'stock' => 'required|integer|min:0',
    'user_id' => 'required|exists:users,id'
  ];

  // GetAllData
  public function index()
  {
    return response()->json([
      'success' => true,
      'data' => Product::orderBy('id', 'desc')->limit(10)->get()  // Get the last 10 products
    ], Response::HTTP_OK);
  }

  //InsertData
  public function store(Request $request)
  {
    $validator = Validator::make($request->all(), $this->rules);

    if ($validator->fails()) {
      return response()->json([
        'success' => false,
        'message' => 'Validation failed',
        'errors' => $validator->errors()
      ], Response::HTTP_BAD_REQUEST);
    }

    $product = Product::create($validator->validated());

    return response()->json([
      'success' => true,
      'data' => $product
    ], Response::HTTP_CREATED);
  }

  //GetSingleData
  public function show(Product $product)
  {
    return response()->json([
      'success' => true,
      'data' => $product
    ], Response::HTTP_OK);
  }

  //UpdateData
  public function update(Request $request, Product $product)
  {
    // Get the rules of the fields coming from the Request
    $updateRules = array_intersect_key($this->rules, $request->all());
    $validator = Validator::make($request->all(), $updateRules);

    if ($validator->fails()) {
      return response()->json([
        'success' => false,
        'message' => 'Validation failed',
        'errors' => $validator->errors()
      ], Response::HTTP_BAD_REQUEST);
    }

    $product->update($validator->validated());

    return response()->json([
      'success' => true,
      'data' => $product
    ], Response::HTTP_OK);
  }

  //DeleteData
  public function destroy(Product $product)
  {
    $product->delete();

    return response()->json([
      'success' => true,
      'message' => 'Product deleted successfully'
    ], Response::HTTP_NO_CONTENT);
  }
};

7. Database Seeder

<?php

namespace Database\Seeders;

use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
  /**
   * Seed the application's database.
   */
  public function run(): void
  {
    $this->call([
      SuperAdminSeeder::class,
      UserSeeder::class,
      ProductSeeder::class
    ]);
  }
}

8. Run Migrations and Seeders

php artisan migrate:fresh --seed

NOTE: To run specific seeder: php artisan db:seed ProductSeeder or php artisan db:seed --class=ProductSeeder

9. Configure Route Provider

php artisan make:provider RouteServiceProvider

NOTE: This will create app/Providers/RouteServiceProvider.php and also add App\Providers\RouteServiceProvider::class line to bootstrap/providers.php.

<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
  public function boot(): void
  {
    RateLimiter::for('api', function (Request $request) {
      return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });

    $this->routes(function () {
      Route::middleware('api')
        ->prefix('api')
        ->group(base_path('routes/api.php'));

      Route::middleware('web')
        ->group(base_path('routes/web.php'));
    });
  }
}

10. Create API Routes

touch routes/api.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductController;

Route::apiResource('products', ProductController::class);

Testing with Postman

Available Endpoints

Method Endpoint Description
GET /api/products List all products
GET /api/products/{id} Get single product
POST /api/products Create new product
PUT/PATCH /api/products/{id} Update product
DELETE /api/products/{id} Delete product

Example Requests

Create Product (POST)

POST /api/products
Content-Type: application/json

{
    "name": "Test Product",
    "description": "Test product description",
    "price": 149.99,
    "stock": 25
}

Response Status Codes:

Update Product (PUT)

PUT /api/products/1
Content-Type: application/json

{
    "name": "Updated Product",
    "price": 199.99
}

Response Status Codes:

Response Format

{
  "success": true,
  "data": {
    "id": 1,
    "name": "Test Product",
    "description": "Test product description",
    "price": "149.99",
    "stock": 25,
    "created_at": "2025-01-09T13:41:01.000000Z",
    "updated_at": "2025-01-09T13:41:01.000000Z"
  }
}

Error Response Format:

{
  "success": false,
  "message": "Validation failed",
  "errors": {
    "field": ["Error message"]
  }
}

Advanced Concepts

1. Validation Rules

2. Rate Limiting

3. Error Handling

Running the Application

  1. Start development server:

    php artisan serve
    
  2. Clear various caches if needed:

    php artisan optimize:clear    # Clear all caches
    php artisan config:clear     # Clear config cache
    php artisan route:clear      # Clear route cache
    php artisan cache:clear      # Clear application cache
    
  3. View all routes:

    php artisan route:list
    

Best Practices

  1. Response Format Consistency

    • Always use consistent JSON structure
    • Include success status in response
    • Use appropriate HTTP status codes
  2. Validation

    • Always validate input data
    • Use Laravel’s built-in validation
    • Return clear validation error messages
  3. Route Naming

    • Use resource routes when possible
    • Follow REST conventions
    • Use appropriate HTTP methods
  4. Security

    • Implement rate limiting
    • Validate all input
    • Use HTTPS in production

Common Issues and Solutions

  1. Routes Not Working

    • Check RouteServiceProvider configuration
    • Clear route cache
    • Ensure API prefix is correct
  2. Validation Errors

    • Check request data format
    • Verify validation rules
    • Check error messages in response
  3. Database Issues

    • Verify database configuration
    • Run migrations
    • Run seeder when needed
    • Check file permissions for SQLite

Next Steps

  1. Adding relationships between models
  2. Implement caching
  3. Implement JWT authentication
  4. Add authentication (Laravel Sanctum)
  5. Social Authentication with Laravel Socialite
  6. WebSocket with Laravel Echo
  7. Add API documentation (e.g., Swagger)
  8. Add more complex validation
  9. Implement API versioning

Additional Resources

Suppress All Errors and Return JSON

mkdir app/Exceptions
touch app/Exceptions/Handler.php
<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

class Handler extends ExceptionHandler
{
  public function register(): void
  {
    $this->renderable(function (Throwable $e) {
      return new JsonResponse([
        'success' => false,
        'message' => $e->getMessage(),
        'code' => $e->getCode() ?: Response::HTTP_INTERNAL_SERVER_ERROR
      ], Response::HTTP_INTERNAL_SERVER_ERROR);
    });
  }
}