Getting Started

Build Your First API

Step-by-step tutorial to build a complete REST API

Build a complete task management API from scratch with authentication, validation, and database operations.

What You'll Build

A RESTful API with:

  • User authentication (JWT)
  • CRUD operations for tasks
  • Input validation
  • Database migrations
  • Error handling

Time: 15 minutes

Prerequisites

  • Glueful installed (Installation Guide)
  • Basic PHP knowledge
  • Database configured (MySQL, PostgreSQL, or SQLite)

Step 1: Create Project

composer create-project glueful/api-skeleton task-api
cd task-api

Configure .env:

APP_NAME="Task API"
APP_ENV=development
APP_DEBUG=true

DB_DRIVER=mysql
DB_HOST=localhost
DB_DATABASE=task_api
DB_USERNAME=root
DB_PASSWORD=

Step 2: Create Database Migration

Create users table:

php glueful migrate:create CreateUsersTable

Edit the generated migration in database/migrations/:

<?php

use Glueful\Database\Schema\Interfaces\SchemaBuilderInterface;
use Glueful\Database\Migrations\MigrationInterface;

class CreateUsersTable implements MigrationInterface
{
    public function up(SchemaBuilderInterface $schema): void
    {
        $schema->createTable('users', function ($table) {
            $table->bigInteger('id')->primary()->autoIncrement();
            $table->string('uuid', 16)->unique();
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->timestamps();
        });
    }

    public function down(SchemaBuilderInterface $schema): void
    {
        $schema->dropTable('users');
    }

    public function getDescription(): string
    {
        return 'Create users table';
    }
}

Create tasks table:

php glueful migrate:create CreateTasksTable
<?php

use Glueful\Database\Schema\Interfaces\SchemaBuilderInterface;
use Glueful\Database\Migrations\MigrationInterface;

class CreateTasksTable implements MigrationInterface
{
    public function up(SchemaBuilderInterface $schema): void
    {
        $schema->createTable('tasks', function ($table) {
            $table->bigInteger('id')->primary()->autoIncrement();
            $table->string('uuid', 16)->unique();
            $table->bigInteger('user_id');
            $table->string('title');
            $table->text('description')->nullable();
            $table->enum('status', ['pending', 'in_progress', 'completed'])->default('pending');
            $table->timestamp('due_date')->nullable();
            $table->timestamps();
            $table->index('user_id');
            $table->index('status');
        });
    }

    public function down(SchemaBuilderInterface $schema): void
    {
        $schema->dropTable('tasks');
    }

    public function getDescription(): string
    {
        return 'Create tasks table';
    }
}

Run migrations:

php glueful migrate:run

Step 3: Create Controllers

Create auth controller:

app/Controllers/AuthController.php:

<?php

namespace App\Controllers;

use Glueful\Http\Request; // Adjust if using Symfony Request wrapper
use Glueful\Http\Response;
use Glueful\Auth\JWTService;
use Glueful\Helpers\Utils;

class AuthController
{
    public function register(Request $request)
    {
    $validated = $this->validate($request->all(), [
            'name' => 'required|string|min:2',
            'email' => 'required|email|unique:users,email',
            'password' => 'required|string|min:8',
        ]);

        $idData = [
            'uuid' => Utils::generateNanoID(16),
        ];
        $insertData = [
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => password_hash($validated['password'], PASSWORD_BCRYPT),
        ];
        db()->from('users')->insert(array_merge($idData, $insertData));
        $user = db()->from('users')->where('email', $validated['email'])->limit(1)->get()[0] ?? null;

        $token = JWTService::generate([
            'user_id' => $user['id'] ?? null,
            'email' => $user['email'] ?? null,
            'uuid' => $user['uuid'] ?? null,
        ]);

        return Response::created([
            'user' => [
                'id' => $user['uuid'],
                'name' => $user['name'],
                'email' => $user['email'],
            ],
            'token' => $token,
        ]);
    }

    public function login(Request $request)
    {
        $validated = $this->validate($request->all(), [
            'email' => 'required|email',
            'password' => 'required',
        ]);

        $user = db()->from('users')
            ->where('email', $validated['email'])
            ->limit(1)
            ->get()[0] ?? null;

        if (!$user || !password_verify($validated['password'], $user['password'])) {
            return Response::error('Invalid credentials', 401);
        }

        $token = JWTService::generate([
            'user_id' => $user['id'],
            'email' => $user['email'],
            'uuid' => $user['uuid'] ?? null,
        ]);

        return Response::success([
            'user' => [
                'id' => $user['uuid'],
                'name' => $user['name'],
                'email' => $user['email'],
            ],
            'token' => $token,
        ]);
    }

    protected function validate(array $data, array $rules)
    {
        $validator = new \Glueful\Validation\Validator($data, $rules);

        if (!$validator->passes()) {
            Response::error('Validation failed', 422, [
                'errors' => $validator->errors()
            ])->send();
            exit;
        }

        return $validator->validated();
    }
}

Create task controller:

app/Controllers/TaskController.php:

<?php

namespace App\Controllers;

use Glueful\Http\Request;
use Glueful\Http\Response;

class TaskController
{
    public function index(Request $request)
    {
        $authUser = app(Glueful\Auth\AuthenticationManager::class)->user();
        $userId = $authUser?->id ?? null;
        $tasks = db()->from('tasks')
            ->where('user_id', $userId)
            ->orderBy('created_at', 'DESC')
            ->get();

        $mapped = array_map(fn($task) => [
            'id' => $task['uuid'] ?? null,
            'title' => $task['title'] ?? null,
            'description' => $task['description'] ?? null,
            'status' => $task['status'] ?? null,
            'due_date' => $task['due_date'] ?? null,
            'created_at' => $task['created_at'] ?? null,
        ], $tasks ?? []);

        return Response::success([
            'tasks' => $mapped,
        ]);
    }

    public function store(Request $request)
    {
        $validated = $this->validate($request->all(), [
            'title' => 'required|string|min:3|max:255',
            'description' => 'nullable|string',
            'status' => 'nullable|in:pending,in_progress,completed',
            'due_date' => 'nullable|date',
        ]);

        $authUser = app(Glueful\Auth\AuthenticationManager::class)->user();
        $taskData = [
            'uuid' => Utils::generateNanoID(12),
            'user_id' => $authUser?->id,
            'title' => $validated['title'],
            'description' => $validated['description'] ?? null,
            'status' => $validated['status'] ?? 'pending',
            'due_date' => $validated['due_date'] ?? null,
        ];
        db()->from('tasks')->insert($taskData);
        $task = db()->from('tasks')->where('uuid', $taskData['uuid'])->limit(1)->get()[0] ?? null;

        return Response::created([
            'task' => [
                'id' => $task['uuid'],
                'title' => $task['title'],
                'description' => $task['description'],
                'status' => $task['status'],
                'due_date' => $task['due_date'],
            ],
        ]);
    }

    public function show(Request $request, $uuid)
    {
        $authUser = app(Glueful\Auth\AuthenticationManager::class)->user();
        $task = db()->from('tasks')
            ->where('uuid', $uuid)
            ->where('user_id', $authUser?->id)
            ->limit(1)
            ->get()[0] ?? null;

        if (!$task) {
            return Response::error('Task not found', 404);
        }

        return Response::success([
            'task' => [
                'id' => $task['uuid'],
                'title' => $task['title'],
                'description' => $task['description'],
                'status' => $task['status'],
                'due_date' => $task['due_date'],
                'created_at' => $task['created_at'],
            ],
        ]);
    }

    public function update(Request $request, $uuid)
    {
        $authUser = app(Glueful\Auth\AuthenticationManager::class)->user();
        $task = db()->from('tasks')
            ->where('uuid', $uuid)
            ->where('user_id', $authUser?->id)
            ->limit(1)
            ->get()[0] ?? null;

        if (!$task) {
            return Response::error('Task not found', 404);
        }

        $validated = $this->validate($request->all(), [
            'title' => 'sometimes|required|string|min:3|max:255',
            'description' => 'nullable|string',
            'status' => 'sometimes|in:pending,in_progress,completed',
            'due_date' => 'nullable|date',
        ]);

        db()->from('tasks')
            ->where('id', $task['id'])
            ->update($validated);

        $updated = db()->from('tasks')->where('id', $task['id'])->limit(1)->get()[0] ?? null;

        return Response::success([
            'task' => [
                'id' => $updated['uuid'],
                'title' => $updated['title'],
                'description' => $updated['description'],
                'status' => $updated['status'],
                'due_date' => $updated['due_date'],
            ],
        ]);
    }

    public function destroy(Request $request, $uuid)
    {
        $authUser = app(Glueful\Auth\AuthenticationManager::class)->user();
        $task = db()->from('tasks')
            ->where('uuid', $uuid)
            ->where('user_id', $authUser?->id)
            ->limit(1)
            ->get()[0] ?? null;

        if (!$task) {
            return Response::error('Task not found', 404);
        }

    db()->from('tasks')->where('id', $task['id'])->delete();

    return Response::noContent();
    }

    protected function validate(array $data, array $rules)
    {
        $validator = new \Glueful\Validation\Validator($data, $rules);

        if (!$validator->passes()) {
            Response::error('Validation failed', 422, [
                'errors' => $validator->errors()
            ])->send();
            exit;
        }

        return $validator->validated();
    }
}

Step 4: Define Routes

Edit routes/api.php:

<?php

use App\Controllers\AuthController;
use App\Controllers\TaskController;

// Public routes
$router->post('/auth/register', [AuthController::class, 'register']);
$router->post('/auth/login', [AuthController::class, 'login']);

// Protected routes (require authentication)
$router->group(['middleware' => 'auth'], function ($router) {
    // Task management
    $router->get('/tasks', [TaskController::class, 'index']);
    $router->post('/tasks', [TaskController::class, 'store']);
    $router->get('/tasks/{uuid}', [TaskController::class, 'show']);
    $router->put('/tasks/{uuid}', [TaskController::class, 'update']);
    $router->delete('/tasks/{uuid}', [TaskController::class, 'destroy']);
});

Step 5: Test Your API

Start the development server:

php glueful serve

Register a user:

curl -X POST http://localhost:8000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "[email protected]",
    "password": "password123"
  }'

Response:

{
  "success": true,
  "data": {
    "user": {
      "id": "abc123",
      "name": "John Doe",
      "email": "[email protected]"
    },
    "token": "eyJ0eXAiOiJKV1QiLCJhbGc..."
  }
}

Create a task:

curl -X POST http://localhost:8000/api/tasks \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -d '{
    "title": "Complete documentation",
    "description": "Finish writing API docs",
    "status": "in_progress",
    "due_date": "2024-12-31"
  }'

Get all tasks:

curl http://localhost:8000/api/tasks \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

Update a task:

curl -X PUT http://localhost:8000/api/tasks/abc123 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -d '{
    "status": "completed"
  }'

Delete a task:

curl -X DELETE http://localhost:8000/api/tasks/abc123 \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

What You Built

✅ Complete REST API with authentication ✅ Database schema with migrations ✅ Input validation ✅ Error handling ✅ JWT authentication ✅ CRUD operations

Next Steps

Full API Reference

Authentication Endpoints

MethodEndpointDescription
POST/auth/registerRegister new user
POST/auth/loginLogin user

Task Endpoints

MethodEndpointAuthDescription
GET/tasksList all tasks
POST/tasksCreate task
GET/tasks/{id}Get task
PUT/tasks/{id}Update task
DELETE/tasks/{id}Delete task

Common Issues

Database connection error?

  • Check .env database credentials
  • Ensure database exists
  • Run php glueful migrate:run

401 Unauthorized?

  • Include Authorization: Bearer TOKEN header
  • Check token hasn't expired
  • Verify JWT_SECRET in .env

Validation errors?

  • Check request payload matches validation rules
  • Ensure Content-Type is application/json