A RESTful task management API built with vanilla PHP — no framework. Uses JSON files for storage, JWT-style bearer token authentication, and ships with a simple browser UI.
Built as a learning project to understand PHP fundamentals: routing, middleware, MVC structure, and containerisation with Docker.
| Concern | Choice |
|---|---|
| Language | PHP 8.2 |
| Routing | Custom (no framework) |
| Auth | Bearer token (stored in JSON) |
| Storage | JSON flat files |
| IDs | UUID v4 via ramsey/uuid |
| Config | vlucas/phpdotenv |
| Container | Docker + Docker Compose |
php-task-api-learning-project/
│
├── public/ # Web root — only this directory is exposed
│ ├── index.php # Application entry point (front controller)
│ └── app.html # Standalone HTML playground
│
├── src/ # All application code (PSR-4 autoloaded as App\)
│ ├── Controllers/
│ │ ├── Controller.php # Base controller (shared helpers: json(), input(), etc.)
│ │ ├── AuthController.php # Handles register / login / me / logout
│ │ ├── TaskController.php # CRUD for tasks
│ │ └── ViewController.php # Serves PHP view pages
│ │
│ ├── Middleware/
│ │ └── Auth.php # Validates Bearer token, returns user or 401
│ │
│ ├── Models/
│ │ ├── User.php # Read/write users.json
│ │ └── Task.php # Read/write tasks.json
│ │
│ ├── Routes/
│ │ ├── Router.php # Dispatches request to the right route file
│ │ ├── AuthRoutes.php # /api/auth/* routes
│ │ ├── TaskRoutes.php # /api/tasks/* routes
│ │ ├── HealthRoutes.php # /api/health
│ │ └── WebRoutes.php # Browser page routes (/)
│ │
│ └── Views/
│ ├── layout.php # Shared HTML shell (head, scripts)
│ ├── home.php # Task board page
│ ├── login.php # Login page
│ ├── signup.php # Registration page
│ ├── profile.php # User profile page
│ ├── partials/
│ │ ├── header.php # Page header partial
│ │ ├── nav.php # Navigation partial
│ │ ├── board.php # Task board partial
│ │ └── form.php # Task form partial
│ └── scripts/
│ ├── app.js # Task board JS (fetch calls)
│ └── auth.js # Auth JS (login / register / logout)
│
├── storage/ # Flat-file database (gitignored in production)
│ ├── users.json # Registered users
│ ├── tasks.json # All tasks
│ └── tokens.json # Active auth tokens
│
├── vendor/ # Composer dependencies (not committed)
├── composer.json
├── composer.lock
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env # Local environment config (never commit this)
└── .gitignore
Prerequisites: Docker Desktop installed and running.
-
Clone the repo:
git clone <repo-url> cd php-task-api-learning-project
-
Create your
.envfile:cp .env.example .env # or create it manually — see Environment Variables below -
Start the container:
docker compose up --build
-
Visit http://localhost:8000
To stop: docker compose down
Prerequisites: PHP 8.2+, Composer.
-
Install dependencies:
composer install
-
Create your
.envfile (see below). -
Start the dev server:
composer dev
-
Visit http://localhost:8000
Create a .env file in the project root:
APP_NAME="Task Manager"
APP_URL=http://localhost:8000| Variable | Description | Default |
|---|---|---|
APP_NAME |
Application display name | Task Manager |
APP_URL |
Base URL (used for CORS and links) | http://localhost:8000 |
When running via Docker, these are injected by
docker-compose.ymlusingenv_file. The.envfile does not need to exist inside the container.
All API endpoints return JSON. Protected routes require an Authorization: Bearer <token> header — the token is returned on login.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
No | Create a new account |
| POST | /api/auth/login |
No | Log in, receive a token |
| GET | /api/auth/me |
Yes | Get the current user |
| POST | /api/auth/logout |
Yes | Invalidate the token |
Register / Login request body:
{
"email": "user@example.com",
"password": "secret"
}Login response:
{
"token": "some-uuid-token",
"user": { "id": "...", "email": "user@example.com" }
}All task routes require authentication.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tasks |
List all tasks for the user |
| POST | /api/tasks |
Create a new task |
| GET | /api/tasks/:id |
Get a single task |
| PUT | /api/tasks/:id |
Update a task |
| DELETE | /api/tasks/:id |
Delete a task |
Create / Update request body:
{
"title": "Buy groceries",
"description": "Milk, eggs, bread",
"status": "pending"
}Task object:
{
"id": "uuid-v4",
"user_id": "uuid-v4",
"title": "Buy groceries",
"description": "Milk, eggs, bread",
"status": "pending",
"created_at": "2026-05-21T15:00:00+00:00"
}Request lifecycle:
HTTP Request
└── public/index.php ← front controller, loads .env
└── Router.php ← matches method + path
├── AuthRoutes / TaskRoutes / etc.
│ └── Controller method
│ ├── Middleware\Auth (if protected)
│ ├── Model (read/write JSON)
│ └── json() response
└── 404 if no route matched
Storage: Data is persisted in storage/*.json files. The storage/ directory is mounted as a Docker volume so data survives container restarts.
Authentication: On login, a UUID token is generated and stored in storage/tokens.json mapped to the user's ID. Protected routes run Middleware\Auth, which reads the Authorization header, looks up the token, and returns the associated user — or a 401 if invalid.