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
- Validation - Advanced validation rules
- Authentication - Auth customization
- Testing - Write API tests
- Deployment - Deploy to production
Full API Reference
Authentication Endpoints
Method | Endpoint | Description |
---|---|---|
POST | /auth/register | Register new user |
POST | /auth/login | Login user |
Task Endpoints
Method | Endpoint | Auth | Description |
---|---|---|---|
GET | /tasks | ✓ | List all tasks |
POST | /tasks | ✓ | Create 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