laravel 13 rest api easy tutorial Binaryboxtuts

Laravel 13 REST API Easy Tutorial (Step-by-Step)

Avatar photoPosted by

Introduction:

Hi there! In this blog, I will be showing you how to make a REST API using Laravel 13. We will build a small Projects API where a client (a mobile app, a React or Vue frontend, or even Postman) can create, list, view, update, and delete projects using JSON.

REST API (also called a RESTful API) is a way for a client and a server to talk to each other over HTTP. REST stands for Representational State Transfer and API stands for Application Programming Interface. In a REST API, each URL represents a resource (like /api/projects), and the HTTP method tells the server what to do with it: GET to read, POST to create, PUT/PATCH to update, and DELETE to remove. The server is stateless, which means every request carries everything the server needs to handle it.

Laravel is a free, open-source PHP framework that follows the MVC pattern and comes with routing, the Eloquent ORM, validation, and much more out of the box. Laravel 13 was released in March 2026 and requires PHP 8.3 or higher. If you followed my Laravel 12 REST API tutorial, this is the updated version. This time, I am doing things the “proper” way with Form Requests for validation, API Resources for clean JSON, route model binding, pagination, correct HTTP status codes, and even a few automated tests.

Prerequisite:

Before we proceed, make sure these are installed and updated on your local machine:

  • PHP >= 8.3
  • Composer >= 2.8
  • Laravel Installer >= 5.x (latest)
  • MySQL or MariaDB (or just use the default SQLite)
  • Postman, Insomnia, or cURL for testing the API

If you need to update the Laravel Installer, run:

composer global require laravel/installer

Step 1: Install Laravel 13

Open your Terminal or CMD and run this command to create a new Laravel 13 project:

laravel new laravel-13-rest-api

Since we are building an API, we don’t need a frontend. These are the options I picked:

  • Starter kit: None
  • Testing framework: PHPUnit (the tests in Step 9 use PHPUnit)
  • Database: MySQL (or SQLite)
  • Run the default database migrations: Yes

Then go inside the project folder:

cd laravel-13-rest-api

Step 2: Set Database Configuration

Open the .env file in the project root and check the database settings. If you chose MySQL, it should look like this:

.env

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_13_rest_api
DB_USERNAME=root
DB_PASSWORD=

Make sure the database exists in your MySQL server. If you want to keep it simple, leave DB_CONNECTION=sqlite and everything in this tutorial will work the same way.

Step 3: Enable API Routing

A fresh Laravel 13 app doesn’t have a routes/api.php file. We enable API routing with this Artisan command:

php artisan install:api

This command creates routes/api.php, registers it in bootstrap/app.php, and installs Laravel Sanctum (which we can use later for token authentication). Every route in this file automatically gets the /api prefix. A nice bonus: the default Laravel 13 bootstrap/app.php already tells Laravel to render errors as JSON for any api/* request, so a missing record returns a JSON 404 instead of an HTML page.

Step 4: Create Model and Migration

Run this command to create a Project model with its migration:

php artisan make:model Project --migration

Open the new migration file inside database/migrations/ (it ends with _create_projects_table.php) and add the columns. I added a status column so our API has something to update:

database/migrations/xxxx_xx_xx_xxxxxx_create_projects_table.php

<?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('projects', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->string('status')->default('pending');
            $table->timestamps();
        });
    }

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

Next, update the model. In Laravel 13 we can declare mass-assignable fields with the #[Fillable] PHP attribute. I also set a default status on the model, so the JSON we return right after creating a project already shows "pending" (otherwise it would be null until the record is reloaded):

app/Models/Project.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;

#[Fillable(['name', 'description', 'status'])]
class Project extends Model
{
    /**
     * The model's default values for attributes.
     *
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'pending',
    ];
}

Run the migration to create the table:

php artisan migrate

Step 5: Create Form Requests for Validation

A Form Request is a class that holds the validation rules for a request. It keeps our controller clean, and when validation fails on an API request, Laravel automatically returns a 422 JSON response with the error messages.

We don’t need to create them by hand. In the next step, the --requests flag will generate them for us. For now, here is what they should contain. Notice that authorize() returns true. The generated file returns false, which would block every request with a 403 error!

app/Http/Requests/StoreProjectRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;

class StoreProjectRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
            'status' => ['sometimes', 'in:pending,in_progress,completed'],
        ];
    }
}

For updates, I used sometimes so the client can send only the fields it wants to change (perfect for PATCH requests):

app/Http/Requests/UpdateProjectRequest.php

<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;

class UpdateProjectRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'name' => ['sometimes', 'required', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
            'status' => ['sometimes', 'in:pending,in_progress,completed'],
        ];
    }
}

Step 6: Create an API Resource

An API Resource is a transformation layer between your Eloquent model and the JSON your users see. Instead of dumping the whole database row, we choose exactly which fields to expose and how they are formatted. Run:

php artisan make:resource ProjectResource

Open the generated file and update the toArray method:

app/Http/Resources/ProjectResource.php

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProjectResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'description' => $this->description,
            'status' => $this->status,
            'created_at' => $this->created_at?->toDateTimeString(),
            'updated_at' => $this->updated_at?->toDateTimeString(),
        ];
    }
}

Step 7: Create the API Controller

Now let’s create the controller. The --api flag generates only the five methods an API needs (no create and edit form methods), --model enables route model binding, and --requests generates the two Form Requests from Step 5:

php artisan make:controller Api/ProjectController --api --model=Project --requests

Open the controller and replace its contents with this code. (Don’t forget to update the two request files with the code from Step 5.)

app/Http/Controllers/Api/ProjectController.php

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProjectRequest;
use App\Http\Requests\UpdateProjectRequest;
use App\Http\Resources\ProjectResource;
use App\Models\Project;

class ProjectController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return ProjectResource::collection(Project::latest()->paginate(10));
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreProjectRequest $request)
    {
        $project = Project::create($request->validated());

        return (new ProjectResource($project))
            ->response()
            ->setStatusCode(201);
    }

    /**
     * Display the specified resource.
     */
    public function show(Project $project)
    {
        return new ProjectResource($project);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateProjectRequest $request, Project $project)
    {
        $project->update($request->validated());

        return new ProjectResource($project);
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Project $project)
    {
        $project->delete();

        return response()->noContent();
    }
}

Here is what each method returns:

  • index: a paginated list (10 per page) wrapped in data, plus links and meta for pagination.
  • store: the new project with a 201 Created status code.
  • show: a single project. If the ID doesn’t exist, route model binding returns a JSON 404.
  • update: the updated project with 200 OK.
  • destroy: an empty 204 No Content response.

Want to go deeper into controllers? Check out my Laravel controllers explained guide.

Step 8: Register the API Routes

Open routes/api.php and register a resource route with Route::apiResource():

routes/api.php

<?php

use App\Http\Controllers\Api\ProjectController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');

Route::apiResource('projects', ProjectController::class);

That one line creates all five endpoints. You can list them with:

php artisan route:list --path=api/projects

You should see these routes:

  • GET /api/projects: list projects
  • POST /api/projects: create a project
  • GET /api/projects/{project}: show a project
  • PUT/PATCH /api/projects/{project}: update a project
  • DELETE /api/projects/{project}: delete a project

If routing is still new to you, read my Laravel routing tutorial for beginners.

Step 9: Run and Test the API

Start the development server:

php artisan serve

Now let’s test the endpoints. I am using cURL here, but you can send the same requests from Postman. Always send the Accept: application/json header so Laravel knows you want JSON back.

# Create a project
curl -X POST http://127.0.0.1:8000/api/projects \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"name":"Website Redesign","description":"Redesign the company website"}'

# List projects
curl http://127.0.0.1:8000/api/projects -H "Accept: application/json"

# Show a project
curl http://127.0.0.1:8000/api/projects/1 -H "Accept: application/json"

# Update only the status
curl -X PATCH http://127.0.0.1:8000/api/projects/1 \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"status":"in_progress"}'

# Delete a project
curl -X DELETE http://127.0.0.1:8000/api/projects/1 -H "Accept: application/json"

The create request returns a 201 status with the new project inside a data key, like this:

{"data":{"id":1,"name":"Website Redesign","description":"Redesign the company website","status":"pending","created_at":"2026-10-23 01:09:46","updated_at":"2026-10-23 01:09:46"}}

If you send a POST request without a name, you get a 422 response with the validation errors:

{"message":"The name field is required.","errors":{"name":["The name field is required."]}}

Automated tests (optional but recommended)

Clicking around in Postman is fine, but a few feature tests make sure your API keeps working as it grows. Create a test file:

php artisan make:test ProjectApiTest

Then add these tests:

tests/Feature/ProjectApiTest.php

<?php

namespace Tests\Feature;

use App\Models\Project;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ProjectApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_it_creates_a_project(): void
    {
        $response = $this->postJson('/api/projects', [
            'name' => 'Website Redesign',
            'description' => 'Redesign the company website',
        ]);

        $response->assertCreated()
            ->assertJsonPath('data.name', 'Website Redesign')
            ->assertJsonPath('data.status', 'pending');
    }

    public function test_name_is_required(): void
    {
        $this->postJson('/api/projects', [])
            ->assertUnprocessable()
            ->assertJsonValidationErrors(['name']);
    }

    public function test_it_updates_and_deletes_a_project(): void
    {
        $project = Project::create(['name' => 'Mobile App']);

        $this->putJson("/api/projects/{$project->id}", ['status' => 'completed'])
            ->assertOk()
            ->assertJsonPath('data.status', 'completed');

        $this->deleteJson("/api/projects/{$project->id}")->assertNoContent();

        $this->getJson("/api/projects/{$project->id}")->assertNotFound();
    }
}

Run them with:

php artisan test

The RefreshDatabase trait resets the database for each test, so your real data is never touched (with the default phpunit.xml, tests run on an in-memory SQLite database).

Conclusion:

And that’s it! We have built a clean Laravel 13 REST API with a model and migration, Form Requests for validation, an API Resource for consistent JSON, an API resource controller with route model binding and proper status codes, and feature tests to back it all up.

From here, the next step is usually protecting the endpoints. Sanctum is already installed, so you can add ->middleware('auth:sanctum') to the route once you have token authentication set up. Want a version with a UI? Check out my Laravel 13 CRUD app tutorial. If you have any questions, leave a comment below. Happy coding!