- Register
- Login
- Logout
- Fields: name, description, status (active, completed, archived)
- Create project
- List projects
- View project
- Update project
- Delete project
- Scope projects to the authenticated user
- Fields: title, description, priority (low, medium, high), status (todo, in_progress, done), due_date
- Create task
- Update task
- Delete task
- List tasks
- Filter by status
- Filter by priority
- Search by title
- Single endpoint returning: total projects, active projects, total tasks, completed tasks, pending tasks, overdue tasks
- Laravel 11+ / latest stable
- REST API
- API Resource classes
- Form Request validation
- Sanctum authentication
- Eloquent relationships (User hasMany Projects, Project hasMany Tasks)
- Pagination
- Factories
- Seeders with sample data
- Proper HTTP status codes
- Error handling
- Soft deletes
- Git repository with meaningful incremental commits
- Repository pattern
- Service layer
- Swagger / OpenAPI documentation
- Unit / feature tests
- Docker
- Postman collection
- Queue job: notify when a task becomes overdue
- GitHub repository — https://github.com/hamada-emam-tech/task-management-api
- SQL dump or migration files
- README: installation steps, environment setup, API documentation
- Postman collection
- Seeder with sample data
- Completed individually
- Meaningful git commits throughout development (no single final commit)
- Code quality prioritized over extra features
- Delivered within 48 hours of receiving the task
Evidence for each one is in How each criterion is covered.
- Clean code & architecture — 25%
- Laravel best practices — 20%
- Database design — 15%
- API design — 15%
- Validation & error handling — 10%
- Git commits — 5%
- README & documentation — 5%
- Bonus features — 5%
Laravel 13 · PHP 8.3 · MySQL 8.4 · Sanctum · Scramble (OpenAPI) · Docker Compose · PHPUnit · Pint
Scaffolded with composer create-project laravel/laravel, then php artisan install:api for Sanctum and composer require dedoc/scramble for the generated documentation.
| What | Where |
|---|---|
| Swagger UI (interactive, try the endpoints) | http://localhost:8080/docs/api |
| OpenAPI 3.1 document | http://localhost:8080/docs/api.json |
| Postman collection (16 requests, runs green end to end) | docs/postman_collection.json |
| ERD and index rationale | docs/erd.md |
| SQL dump (schema + sample data) | docs/database.sql |
| Demo account | demo@example.com / password |
A request travels through one direction only, and every layer has a single job:
Route (versioned)
→ FormRequest validation, nothing else
→ DTO immutable, framework-free input
→ Service (contract) business rules
→ Repository (contract) persistence, the only place that queries
→ Resource output shape
→ ApiResponse one envelope for the whole API
| Layer | Location | Responsibility |
|---|---|---|
| Routes | routes/api/v1.php | Versioned under /api/v1, named api.v1.*. A v2 is a new file, not an edit. |
| Requests | app/Http/Requests/Api/V1 | Rules only; each exposes toData() so controllers never touch raw input. |
| DTOs | app/DataTransferObjects | final readonly objects passed into services. |
| Contracts | app/Contracts | Controllers depend on service interfaces, services on repository interfaces. Bound in AppServiceProvider. |
| Services | app/Services | Use cases. No HTTP knowledge and no queries, so they stay testable. |
| Repositories | app/Repositories | Every Eloquent query lives here. Swapping the storage never touches a service. |
| Policies | app/Policies | Ownership rules, attached with #[UsePolicy]. |
| Resources | app/Http/Resources/Api/V1 | The only place that decides what a client sees. |
| Envelope | app/Support/ApiResponse.php | success, message, data, meta / errors. |
| Errors | app/Exceptions/ApiExceptionRenderer.php | Every exception on an API route becomes the same error envelope. |
Response shapes:
ERD, relations, and the reasoning behind every index: docs/erd.md
cp .env.example .env
docker compose up -d --build
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate --seedAPI: http://localhost:8080
| Service | Role | Host port |
|---|---|---|
nginx |
Web entry point | 8080 (APP_PORT) |
app |
PHP 8.3 FPM + OPcache | — |
queue |
queue:work for background jobs |
— |
scheduler |
schedule:work for the overdue check |
— |
mysql |
MySQL 8.4, persisted in mysql-data |
3307 (DB_FORWARD_PORT) |
Requires PHP 8.3+ and a MySQL 8 instance.
cp .env.example .env
composer install
php artisan key:generate
php artisan migrate --seed
php artisan serve| Variable | Default | Notes |
|---|---|---|
APP_PORT |
8080 |
Host port nginx binds to. |
DB_CONNECTION |
mysql |
Tests run on in-memory SQLite (phpunit.xml), no setup needed. |
DB_HOST |
mysql |
The compose service name. Use 127.0.0.1 if you run PHP outside Docker. |
DB_PORT |
3306 |
Port inside the network. Outside Docker use DB_FORWARD_PORT. |
DB_FORWARD_PORT |
3307 |
Host port MySQL is published on, so it never clashes with a local 3306. |
DB_DATABASE / DB_USERNAME / DB_PASSWORD |
task_management / task_user / secret |
Compose creates the database and user from these. |
DB_ROOT_PASSWORD |
root |
MySQL root password, container only. |
QUEUE_CONNECTION |
database |
Overdue notifications run through the queue service. |
Swagger UI is generated from the code itself — request rules, resources and return types — so it can never drift from the implementation. No annotations to maintain.
- UI:
http://localhost:8080/docs/api - OpenAPI 3.1 JSON:
http://localhost:8080/docs/api.json
Authenticate in the UI with the token returned by login (Authorization: Bearer <token>).
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/v1/auth/register |
— | Create an account, returns a token. |
POST |
/api/v1/auth/login |
— | Exchange credentials for a token. |
POST |
/api/v1/auth/logout |
Bearer | Revoke the token used for the request. |
GET |
/api/v1/auth/me |
Bearer | The authenticated user. |
GET |
/api/v1/projects |
Bearer | Paginated list of the caller's projects. |
POST |
/api/v1/projects |
Bearer | Create a project. |
GET |
/api/v1/projects/{id} |
Bearer | A single project with its task count. |
PUT/PATCH |
/api/v1/projects/{id} |
Bearer | Update a project (partial allowed). |
DELETE |
/api/v1/projects/{id} |
Bearer | Soft delete a project and its tasks. |
GET |
/api/v1/projects/{id}/tasks |
Bearer | Paginated tasks of one project. |
POST |
/api/v1/projects/{id}/tasks |
Bearer | Add a task to a project. |
GET |
/api/v1/tasks |
Bearer | Every task of the caller, across projects. |
GET |
/api/v1/tasks/{id} |
Bearer | A single task. |
PUT/PATCH |
/api/v1/tasks/{id} |
Bearer | Update a task (partial allowed). |
DELETE |
/api/v1/tasks/{id} |
Bearer | Soft delete a task. |
GET |
/api/v1/dashboard |
Bearer | Summary counters for the caller. |
Query parameters:
| Endpoint | Parameters |
|---|---|
GET /projects |
status (active, completed, archived), per_page (1–100, default 15) |
GET /projects/{id}/tasks and GET /tasks |
status (todo, in_progress, done), priority (low, medium, high), search (title), overdue (boolean), per_page |
Tasks use shallow nesting: they are created and listed inside a project, then addressed directly by id.
tasks:notify-overdue runs hourly from the scheduler container. It queues one NotifyTaskOverdue job per task that just became overdue; the job re-checks the task before sending and stamps overdue_notified_at, so a task is announced exactly once. Rescheduling a task clears the stamp, so it can be announced again.
docker compose exec app php artisan tasks:notify-overdue # run it now
docker compose logs -f queue # watch the workerMail uses the log driver by default, so notifications land in storage/logs/laravel.log.
- Status codes:
200ok,201created,401unauthenticated / bad credentials,403forbidden,404not found,405wrong method,422validation,429rate limited,500unexpected. - Rate limits:
6/minper IP on register and login,60/minper user (or IP) everywhere else. - Ownership is enforced by policies, not by query tricks. Another user's project answers
403, a missing or deleted one answers404. - Pagination always returns
dataas a flat array plusmetawithcurrent_page,per_page,total,last_page. - Tokens expire after
SANCTUM_TOKEN_EXPIRATIONminutes (7 days by default).device_nameis optional on register/login and names the token, so one device can log out without killing the others.
Import docs/postman_collection.json. Run Auth → Login once and the token is stored in the {{token}} collection variable; every other request picks it up. The whole collection also runs unattended:
npx newman run docs/postman_collection.json # 16 requests, 0 failuresdocker compose exec app php artisan test51 feature tests / 160 assertions, on in-memory SQLite (phpunit.xml), so the suite never touches the MySQL database and needs no setup.
| Area | File |
|---|---|
| Registration, login, logout, token revocation | AuthTest.php |
| Project CRUD, ownership, pagination, soft delete cascade | ProjectTest.php |
| Task CRUD, filters, search, overdue listing | TaskTest.php |
| Dashboard totals and its query budget | DashboardTest.php |
| Scheduled command, queued job, exactly-once notification | OverdueNotificationTest.php |
Code style is enforced with Laravel Pint:
docker compose exec app ./vendor/bin/pint --test| Criterion | Where to look |
|---|---|
| Clean code & architecture (25%) | One direction of dependencies: controller → service contract → repository contract. Controllers are 3–6 lines per action, services hold no queries, repositories hold no HTTP. DTOs are final readonly; every class is declare(strict_types=1). |
| Laravel best practices (20%) | Sanctum, policies via #[UsePolicy], query scopes via #[Scope], enum casts, API Resources, Form Requests, container bindings, queued jobs, scheduler, Model::shouldBeStrict() so lazy loading fails loudly instead of becoming an N+1. |
| Database design (15%) | docs/erd.md — normalised tables, FK cascade, soft deletes, and an index per real query pattern with the reasoning written down. |
| API design (15%) | Versioned /api/v1, plural resources, shallow nesting for tasks, correct verbs, one response envelope, pagination meta, filters as query parameters. |
| Validation & error handling (10%) | Every write has a Form Request; enums validated with Rule::enum. ApiExceptionRenderer turns every exception into the same shape, with debug details hidden outside local. |
| Git commits (5%) | ~25 focused commits following Conventional Commits, each one a working step (schema → models → auth → projects → tasks → dashboard → jobs), including the fixes found while testing. |
| README & documentation (5%) | This file, the ERD, the generated Swagger UI, and the Postman collection. |
| Bonus features (5%) | All seven: repository pattern, service layer, OpenAPI docs, tests, Docker, Postman collection, queued overdue notification. |
- Statuses are PHP enums over
stringcolumns, not MySQLENUM— adding a value is a code change, not anALTER TABLEon a large table. - Deleting a project soft deletes its tasks in one transaction, so reports and task queries can never see orphans.
- The dashboard aggregates in SQL (one query per table) instead of counting in PHP; a test enforces the query budget so it cannot regress.
- Overdue notifications are stamped with
overdue_notified_atand re-checked inside the job, because the task may be finished, rescheduled or deleted between queueing and running. - No
datawrapper from Laravel's default resource behaviour — the envelope is explicit and identical for success and failure, so clients parse one shape.