I created a task management API in Laravel to learn the basics (here’s what happened)


I’m learning Laravel for my full-time job and found that the best way to stick with things is to build something real while I’m still figuring it out. It’s not a polished side project. It’s not a “look what I did” showcase. Just me, a terminal, and a lot of “wait, why did that work?”

This is one of those articles.

The project is a task management API, a simple backend that can create, read, update and delete tasks. No interface. No authentication (yet). Just a clean JSON API that taught me more about the core concepts of Laravel than any documentation page.

If you’re learning Laravel and wondering where to start after “Hello World”, this might be exactly what you need.

Why a Tasks API?

When I started delving into Laravel, I came across the same concepts over and over again: routes, controllers, models, migrations, CRUD. Every tutorial, every course, every job description mentioned them. So instead of learning each of them in isolation, I decided to create something that would require me to use them all together.

A task management API is perfect for this. It’s simple enough that you won’t get drowned in business logic, but real enough to touch on all the fundamental concepts that Laravel has to offer.

This is what the API does:

  • GET /api/tasks — search all tasks
  • POST /api/tasks — create a new task
  • GET /api/tasks/string – search for a single task
  • PUT /api/tasks/max:255',
    'description' => 'nullable
    — update a task
  • DELETE /api/tasks/max:255',
    'description' => 'nullable
    — delete a task

Five final points. Complete CRUD. Let’s build it.

What you will need

  • PHP installed locally
  • Laravel project setup (I’m using XAMPP)
  • Postman to test the endpoints.
  • A code editor (I’m using VS Code)

If you are on XAMPP, make sure Apache and MySQL are running in the XAMPP Control Panel before you begin.

Step 1: Navigate to your project

Open the command prompt. In Windows, press Windows + Rguy cmdand press Enter.

Then navigate to your Laravel project folder:

cd C:\xampp\htdocs\your-project-name

In my case it was:

cd C:\Users\your-username\task-manager

Once you’re inside your project directory, you’re ready to start using php artisanthe Laravel command line tool that does a lot of the heavy lifting for you.

Step 2: Create the task model and migration

This was the moment things started to work for me. One command, two files created:

php artisan make:model Task --migration

The terminal output:

Model (app\Models\Task.php) created successfully.
INFO  Migration (database\migrations\2026_06_03_143244_create_tasks_table.php) created successfully.

This is how I understood it:

  • The model (Task.php) is the Laravel messenger. Instead of writing raw SQL like SELECT * FROM tasksyou just call Task::all() and Laravel takes care of the rest.
  • The migration It is the plan. Describe what you tasks the table should be visible. The table does not yet exist in the database. This file simply defines it.

Think about it in three layers:

MySQL Database -> tasks table -> Task model talks to it

The model is not the database. It’s more like the translator between your Laravel code and the data stored in MySQL.

Step 3: Define the table structure

Open the migration file you just created. lives in database/migrations/ and its name starts with a timestamp. Inside you will see this:

public function up(): void
{
    Schema::create('tasks', function (Blueprint $table) string',
        'completed' => 'boolean',
    ));

    $task = Task::create($request->all());
    return response()->json($task, 201);
);
}

By default, Laravel only gives you a id and timestamps. I needed to add columns for the actual task data, so I updated it to this:

Schema::create('tasks', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('description')->nullable();
    $table->boolean('completed')->default(false);
    $table->timestamps();
});
  • title: required, stores the task name
  • description– Optional, longer text about the task
  • completed– A true/false flag, defaults to false when creating a task

Save the file, then run the migration to create the table in MySQL:

php artisan migrate
INFO  Running migrations.
2026_06_03_143244_create_tasks_table ..... 399.03ms DONE

He tasks The table now exists in your database.

Step 4 – Create the controller

The controller is where the logic lives. When someone sends a request to GET /api/tasksthe controller decides what to do. In this case, find all the tasks and return them as JSON.

php artisan make:controller TaskController --api

He --api flag tells Laravel to generate an already structured controller for API use. Open the file in app/Http/Controllers/TaskController.php and you will see five empty methods waiting for you:

public function index()    // GET all tasks
public function store()    // POST create a task
public function show()     // GET one task
public function update()   // PUT update a task
public function destroy()  // DELETE a task

Laravel generated the scaffolding. Now we complete the logic.

Step 5 – Complete the Controller Methods

First, add the task model import to the top of the file:

use App\Models\Task;

Then complete each method one by one.

index() – Get all tasks

public function index()
{
    $tasks = Task::all();
    return response()->json($tasks);
}

Simple. Retrieve everything and return it as JSON.

store() – Create a task

public function store(Request $request)
{
    $request->validate((
        'title' => 'required|string|max:255',
        'description' => 'nullable|string',
        'completed' => 'boolean',
    ));

    $task = Task::create($request->all());
    return response()->json($task, 201);
}

validate() Checks incoming data before touching the database. 201 is the HTTP status code for “created”, which is more precise than the default 200.

show() – Get a task

public function show(string $id)
{
    $task = Task::find($id);

    if (!$task) {
        return response()->json(('message' => 'Task not found'), 404);
    }

    return response()->json($task);
}

If the task does not exist, we return a 404. Otherwise, we return the task.

update() – Updates a task

public function update(Request $request, string $id)
{
    $task = Task::find($id);

    if (!$task) {
        return response()->json(('message' => 'Task not found'), 404);
    }

    $request->validate((
        'title' => 'sometimes|string|max:255',
        'description' => 'nullable|string',
        'completed' => 'boolean',
    ));

    $task->update($request->all());
    return response()->json($task);
}

Warning sometimes in the validation rules. It means “validate this field only if it is present in the request”. That way you can update only the completed been without sending the title and description every time.

destroy() – Delete a task

public function destroy(string $id)
{
    $task = Task::find($id);

    if (!$task) {
        return response()->json(('message' => 'Task not found'), 404);
    }

    $task->delete();
    return response()->json(('message' => 'Task deleted successfully'));
}

Find it. Delete it. Confirm it.

Step 6: Configure API Routes

In Laravel 11, api.php does not exist by default. You need to create it with:

php artisan install:api

This installs Laravel Sanctum (an authentication package you’ll use later) and creates the routes/api.php archive. Don’t worry about Sanctum for now. It’s there but we’re not using it yet.

Open routes/api.php and add your task routes:

use App\Http\Controllers\TaskController;

Route::apiResource('tasks', TaskController::class);

That’s one line. One line that records the five routes automatically.

To verify, run:

php artisan route:list

You should see all five routes registered and assigned to the correct controller methods:

  • GET /api/tasksTaskController@index
  • POST /api/tasksTaskController@store
  • GET /api/tasks/{task}TaskController@show
  • PUT/PATCH /api/tasks/{task}TaskController@update
  • DELETE /api/tasks/{task}TaskController@destroy

Five routes. One line of code. That is Route::apiResource doing his job.

Step 7: Allow bulk assignment

Before we can perform the test, there is one more thing. Open app/Models/Task.php and add a $fillable property:

class Task extends Model
{
    protected $fillable = (
        'title',
        'description',
        'completed',
    );
}

Without this, Laravel crashes Task::create() for security reasons. It is a protection against mass allocation vulnerabilities. adding $fillable It tells Laravel exactly which fields can be filled in this way.

Step 8: Test everything in Postman

Start the development server:

php artisan serve

Then open Postman and test each endpoint.

Create a task (POST)

  • Method: SEND
  • URL: http://127.0.0.1:8000/api/tasks
  • Body (plain JSON):
{
    "title": "My first task",
    "description": "Testing the API"
}

Answer:

{
    "title": "My first task",
    "description": "Testing the API",
    "updated_at": "2026-06-04T10:37:20.000000Z",
    "created_at": "2026-06-04T10:37:20.000000Z",
    "id": 1
}

Laravel assigned an ID and filled in the timestamps automatically. That was a good moment.

Get all tasks (GET)

  • Method: GET
  • URL: http://127.0.0.1:8000/api/tasks

A moment ago this came back (). Now:

(
    {
        "id": 1,
        "title": "My first task",
        "description": "Testing the API",
        "completed": 0,
        "created_at": "2026-06-04T10:37:20.000000Z",
        "updated_at": "2026-06-04T10:37:20.000000Z"
    }
)

"completed": 0 is the default value we set in the migration. false in PHP it becomes 0 in MySQL.

Update a task (PUT)

  • Method: PUT
  • URL: http://127.0.0.1:8000/api/tasks/1
  • Body:
{
    "completed": true
}

Answer:

{
    "id": 1,
    "title": "My first task",
    "description": "Testing the API",
    "completed": true,
    "created_at": "2026-06-04T10:37:20.000000Z",
    "updated_at": "2026-06-04T10:55:23.000000Z"
}

completed now it’s true and updated_at changes automatically. I didn’t touch the timestamp. Laravel just handled it.

Delete a task (DELETE)

  • Method: DELETE
  • URL: http://127.0.0.1:8000/api/tasks/1
{
    "message": "Task deleted successfully"
}

What I really learned

Building this taught me things I couldn’t learn from reading alone.

  • MVC finally made sense. I’ve seen “Model-View-Controller” in the documentation hundreds of times. But when I had to think about where To put the logic in (the route just connects, the controller decides, the model talks to the database), the pattern clicked in a way it never had before.
  • Migrations are just blueprints until you execute them. I kept confusing the migration file with the actual database table. They are not the same. The file describes what the table should look like. php artisan migrate It’s what really builds it.
  • Route::apiResource**is really impressive.** One line that replaces five manual route definitions, each correctly mapped to the correct controller method and HTTP verb. That’s the kind of thing that makes you understand why developers love Laravel.
  • Validation is built-in and easy. I was expecting to write a bunch of manual checks. Instead, $request->validate() It handled everything: required fields, types, maximum lengths and automatically returned a proper error response if something failed.
  • $fillable**it exists for a reason.** At first it seemed like an extra step. but understanding because exists (to prevent mass allocation attacks) made me appreciate that Laravel is thinking about security by default, even at the basics.

Next steps

This is just the beginning. From here, the natural progression is:

  • Authentication with Laravel Sanctum (already installed!)
  • API Resources to transform the look of your JSON responses
  • Query parameters for filtering and classification tasks
  • Relations: What would happen if the tasks belonged to the users?

But for now, I have a working API. Built from scratch. And I understand every line.

That is the goal at this stage. Not to build something perfect, but to build something that makes you understand the fundamentals. Routes, drivers, models, migrations. They are no longer separate concepts. They are a system. And once you see them as a system, everything else in Laravel starts to make sense.

I’m learning Laravel in public as part of my journey towards full development. If you are on a similar path, follow it. I’ll share every step, including the parts where I had no idea what I was doing.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *