Skip to content

Repository files navigation

Habit Tracker API

Habit Tracker API

A RESTful API for habit tracking built with Laravel 11, Laravel Sanctum token authentication, and the Service Repository Pattern architecture.

This API allows users to create a list of habits, perform daily check-ins, calculate streaks, completion rates, and view aggregate statistics — all endpoints are protected by authentication except register and login.

Built with:

Technology Version
PHP ^8.2
Laravel Framework ^11.31
Laravel Sanctum ^4.0
MySQL 8.x

Table of Contents

  1. Features
  2. Service Repository Pattern
  3. Folder Structure
  4. Installation
  5. API Response Format
  6. Response Codes
  7. API Endpoints
  8. Postman Collection
  9. Testing

Features

  • Token-based authentication via Sanctum (register, login, logout).
  • Full habit CRUD with pagination.
  • Flexible frequency: daily (daily) or specific days (specific_days).
  • Check-in / un-check per date with duplicate check-in prevention.
  • Habit statistics: current streak, longest streak, 30-day completion rate, total check-ins.
  • Overview statistics for all active habits.
  • Policy-based authorization: only the habit owner can access their habits.
  • Rate limiting on login/register endpoints (6 requests/minute).
  • Consistent JSON response format.

Service Repository Pattern

What is it?

The Service Repository Pattern is an architectural pattern that separates an application into layers with different responsibilities:

┌─────────────────────────────────────────────────────────────────────┐
│ Request (HTTP)                                                       │
│   │                                                                  │
│   ▼                                                                  │
│ CONTROLLER      → receives request, validates, formats response      │
│   │              (no business logic / DB queries)                    │
│   ▼                                                                  │
│ FORM REQUEST    → input validation                                   │
│ POLICY          → authorization (who can access)                     │
│   │                                                                  │
│   ▼                                                                  │
│ SERVICE         → business logic (application rules)                 │
│   │              (does not know where data comes from)               │
│   ▼                                                                  │
│ REPOSITORY      → data access (DB queries via Eloquent)              │
│ (interface)     → method contract, no implementation                 │
│   │                                                                  │
│   ▼                                                                  │
│ ELOQUENT MODEL  → database table representation                      │
└─────────────────────────────────────────────────────────────────────┘

Request flow

  1. Request enters the Controller.
  2. FormRequest validates the input before it is passed on.
  3. Policy ensures the user only accesses their own data.
  4. The Controller calls the Service — this is where the business logic lives (e.g., duplicate check-in check, streak calculation).
  5. The Service calls the Repository Interface.
  6. RepositoryServiceProvider resolves the interface into an Eloquent implementation.
  7. The repository implementation runs the database query.
  8. The result is passed back up to the controller, wrapped by a Resource, and sent as JSON.

Why is this pattern used?

Benefit Explanation
Separation of concerns Controller (HTTP), Service (business), Repository (data) — one responsibility per layer.
Testability Services can be tested with mocked repositories without touching the database.
Swappable storage Changing the storage only requires creating a new repository implementation and updating the binding.
Consistency Business logic is not duplicated in multiple places.

How the binding works (the key to this pattern)

The repository interfaces are registered with the Laravel container in app/Providers/RepositoryServiceProvider.php:

public function register(): void
{
    $this->app->bind(HabitRepositoryInterface::class, HabitRepository::class);
    $this->app->bind(HabitLogRepositoryInterface::class, HabitLogRepository::class);
}

When Laravel needs a HabitRepositoryInterface (e.g., inside HabitService), it automatically returns a HabitRepository. Without this binding the application would fail, because an interface cannot be instantiated directly.


Folder Structure

app/
├── Enums/
│   └── FrequencyType.php            # Frequency type enum (daily, specific_days)
├── Http/
│   ├── Controllers/Api/V1/         # Controllers for every endpoint
│   │   ├── AuthController.php
│   │   ├── HabitController.php
│   │   ├── HabitLogController.php
│   │   └── StatsController.php
│   ├── Requests/                   # Form validation
│   │   ├── Auth/                   # RegisterRequest, LoginRequest
│   │   └── Habit/                  # StoreHabitRequest, UpdateHabitRequest, CheckInRequest
│   └── Resources/                  # JSON formatters
│       ├── UserResource.php
│       ├── HabitResource.php
│       ├── HabitLogResource.php
│       └── HabitStatsResource.php
├── Models/                         # Eloquent Models
│   ├── User.php
│   ├── Habit.php
│   └── HabitLog.php
├── Policies/
│   └── HabitPolicy.php             # Habit ownership authorization
├── Providers/
│   └── RepositoryServiceProvider.php # Interface → implementation binding
├── Repositories/
│   ├── Contracts/                  # Interfaces (contracts)
│   │   ├── HabitRepositoryInterface.php
│   │   └── HabitLogRepositoryInterface.php
│   └── Eloquent/                   # Implementations
│       ├── HabitRepository.php
│       └── HabitLogRepository.php
└── Services/                       # Business logic
    ├── HabitService.php
    ├── HabitLogService.php
    └── StreakCalculatorService.php

Installation

Prerequisites

  • PHP ^8.2
  • Composer
  • MySQL 8.x

Installation steps

  1. Clone the repository

    git clone https://github.com/yogaarrd/Habit-Tracker-API-Laravel-Service-Repository-Pattern-API.git Laravel-API
    cd Laravel-API
  2. Install PHP dependencies

    composer install
  3. Create the environment file

    cp .env.example .env

    Then adjust the database configuration in .env:

    DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    DB_DATABASE=laravel_habit_tracker_api
    DB_USERNAME=root
    DB_PASSWORD=
  4. Create the MySQL database

    mysql -u root -p -e "CREATE DATABASE laravel_habit_tracker_api"
  5. Generate the application key

    php artisan key:generate
  6. Run migrations + seeder

    php artisan migrate --seed

    The seeder creates a demo account:

    • Email: demo@example.com
    • Password: password
  7. Start the server

    php artisan serve

    The API is accessible at http://127.0.0.1:8000.


API Response Format

All endpoints return JSON with a consistent envelope structure:

{
  "success": true,
  "data": { ... },
  "error": null,
  "meta": { ... }
}
Field Type Description
success boolean true if the request succeeded, false if it failed
data mixed Main payload (object, array, or null)
error string/null Error message (if any)
meta object/null Additional information such as pagination

Example duplicate check-in error response (409):

{
  "success": false,
  "data": null,
  "error": "Habit ini sudah di chek-in untuk tanggal tersebut",
  "meta": null
}

Response Codes

Code Meaning Description
200 OK Request succeeded
201 Created Data was created (new habit, check-in)
401 Unauthorized Token is missing / invalid
403 Forbidden Not allowed (habit belongs to another user)
404 Not Found Habit not found
409 Conflict Duplicate check-in on the same date
422 Unprocessable Entity Validation failed
429 Too Many Requests Too many requests (login/register rate limit)
500 Internal Server Error Server error

API Endpoints

Base URL: http://127.0.0.1:8000/api/v1

Authentication: Send the token in the Authorization: Bearer <token> header for all endpoints except register and login.


1. Authentication

1.1 Register — POST /register

Creates a new account and returns an access token.

Request Body (JSON):

Field Type Required Description
name string User name
email string Unique email
password string At least 8 characters, must contain letters & numbers
password_confirmation string Must match password

Example:

curl -X POST http://127.0.0.1:8000/api/v1/register \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "name": "Budi",
    "email": "budi@example.com",
    "password": "rahasia123",
    "password_confirmation": "rahasia123"
  }'

Response 200:

{
  "success": true,
  "data": {
    "user": {
      "id": 1,
      "name": "Budi",
      "email": "budi@example.com"
    },
    "token": "1|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  "error": null,
  "meta": null
}

1.2 Login — POST /login

Authenticates a registered user.

Request Body (JSON):

Field Type Required Description
email string Registered email
password string Password

Example:

curl -X POST http://127.0.0.1:8000/api/v1/login \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"email": "demo@example.com", "password": "password"}'

Response 200:

{
  "success": true,
  "data": {
    "user": {
      "id": 1,
      "name": "Demo User",
      "email": "demo@example.com"
    },
    "token": "1|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  "error": null,
  "meta": null
}

⚠️ Login and register are limited to 6 requests per minute (throttle:6,1). Code 429 is returned when the limit is exceeded.


1.3 Logout — POST /logout

Revokes the active token. Once logged out, the token can no longer be used.

Example:

curl -X POST http://127.0.0.1:8000/api/v1/logout \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json"

Response 200:

{
  "success": true,
  "data": null,
  "error": null,
  "meta": null
}

1.4 User Profile — GET /user

Returns the profile of the currently authenticated user.

Example:

curl http://127.0.0.1:8000/api/v1/user \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json"

Response 200:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "Demo User",
    "email": "demo@example.com"
  },
  "error": null,
  "meta": null
}

2. Habit (CRUD)

2.1 List Habits — GET /habits

Returns all habits belonging to the authenticated user, ordered by newest, with pagination.

Query Parameters (optional):

Parameter Type Description
is_active boolean Filter 1/true = active, 0/false = inactive
page integer Page number (default 1)

Example:

curl "http://127.0.0.1:8000/api/v1/habits?is_active=1&page=1" \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json"

Response 200:

{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Minum air 2 liter",
      "description": "Target hidrasi harian",
      "frequency_type": "daily",
      "frequency_days": null,
      "is_active": true,
      "created_at": "2026-08-12T06:42:00.000000Z",
      "updated_at": "2026-08-12T06:42:00.000000Z"
    }
  ],
  "error": null,
  "meta": {
    "page": 1,
    "per_page": 15,
    "total": 1
  }
}

2.2 Create Habit — POST /habits

Creates a new habit. Returns code 201.

Request Body (JSON):

Field Type Required Description
name string Habit name
description string Description (max 1000 characters)
frequency_type string daily or specific_days
frequency_days array ⚠️* Required if frequency_type=specific_days. Array of day numbers: 0=Sunday, 1=Monday, ..., 6=Saturday

* frequency_days is automatically null for the daily type.

Example — daily habit:

curl -X POST http://127.0.0.1:8000/api/v1/habits \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "name": "Baca buku 20 menit",
    "description": "Membaca sebelum tidur",
    "frequency_type": "daily"
  }'

Example — specific days habit (Monday & Thursday):

{
  "name": "Berolahraga",
  "frequency_type": "specific_days",
  "frequency_days": [1, 4]
}

Response 201:

{
  "success": true,
  "data": {
    "id": 2,
    "name": "Baca buku 20 menit",
    "description": "Membaca sebelum tidur",
    "frequency_type": "daily",
    "frequency_days": null,
    "is_active": true,
    "created_at": "2026-08-12T07:00:00.000000Z",
    "updated_at": "2026-08-12T07:00:00.000000Z"
  },
  "error": null,
  "meta": null
}

2.3 Habit Detail — GET /habits/{id}

Returns the habit detail along with its statistics.

Example:

curl http://127.0.0.1:8000/api/v1/habits/2 \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json"

Response 200:

{
  "success": true,
  "data": {
    "id": 2,
    "name": "Baca buku 20 menit",
    "description": "Membaca sebelum tidur",
    "frequency_type": "daily",
    "frequency_days": null,
    "is_active": true,
    "created_at": "2026-08-12T07:00:00.000000Z",
    "updated_at": "2026-08-12T07:00:00.000000Z",
    "stats": {
      "current_streak": 3,
      "longest_streak": 5,
      "completion_rate_30d": 70,
      "total_check_ins": 12,
      "checked_in_today": true
    }
  },
  "error": null,
  "meta": null
}

2.4 Update Habit — PUT/PATCH /habits/{id}

Updates habit data. All fields are optional (only the fields sent are updated).

Request Body (JSON):

Field Type Required Description
name string Habit name
description string Description
frequency_type string daily or specific_days
frequency_days array ⚠️* Required if frequency_type=specific_days
is_active boolean true = active, false = inactive

Example — deactivate a habit:

curl -X PATCH http://127.0.0.1:8000/api/v1/habits/2 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"is_active": false}'

Response 200: same as habit detail (without stats).


2.5 Delete Habit — DELETE /habits/{id}

Deletes the habit along with all its check-in logs (cascade).

Example:

curl -X DELETE http://127.0.0.1:8000/api/v1/habits/2 \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json"

Response 200:

{
  "success": true,
  "data": null,
  "error": null,
  "meta": null
}

2.6 Habit Statistics — GET /habits/{id}/stats

Statistics for a single habit.

Example:

curl http://127.0.0.1:8000/api/v1/habits/2/stats \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json"

Response 200:

Field Description
current_streak Number of consecutive fulfilled days up to today
longest_streak Longest streak record of all time
completion_rate_30d Completion percentage for the last 30 days
total_check_ins Total number of check-ins
checked_in_today Whether checked in today
{
  "success": true,
  "data": {
    "current_streak": 3,
    "longest_streak": 5,
    "completion_rate_30d": 70,
    "total_check_ins": 12,
    "checked_in_today": true
  },
  "error": null,
  "meta": null
}

3. Check-in (Habit Log)

3.1 Check-in — POST /habits/{id}/check-in

Records a habit completion on a given date. Returns code 201.

Request Body (JSON):

Field Type Required Description
date string Format YYYY-MM-DD. Default = today. Cannot be later than today
note string Note (max 255 characters)

Example:

curl -X POST http://127.0.0.1:8000/api/v1/habits/2/check-in \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"date": "2026-08-12", "note": "Berhasil hari ini"}'

Response 201:

{
  "success": true,
  "data": {
    "id": 5,
    "logged_at": "2026-08-12",
    "note": "Berhasil hari ini"
  },
  "error": null,
  "meta": null
}

Response 409 (already checked in on that date):

{
  "success": false,
  "data": null,
  "error": "Habit ini sudah di chek-in untuk tanggal tersebut",
  "meta": null
}

3.2 Un-check — DELETE /habits/{id}/check-in

Removes a check-in on a specific date.

Request Body (JSON):

Field Type Required Description
date string Format YYYY-MM-DD. Default = today

Example:

curl -X DELETE http://127.0.0.1:8000/api/v1/habits/2/check-in \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"date": "2026-08-12"}'

Response 200:

{
  "success": true,
  "data": null,
  "error": null,
  "meta": null
}

3.3 Check-in History — GET /habits/{id}/logs

Returns the check-in history with pagination and date filtering.

Query Parameters (optional):

Parameter Type Description
from string Start date filter (YYYY-MM-DD)
to string End date filter (YYYY-MM-DD)
page integer Page number (default 1)

Example:

curl "http://127.0.0.1:8000/api/v1/habits/2/logs?from=2026-07-01&to=2026-08-12&page=1" \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json"

Response 200:

{
  "success": true,
  "data": [
    {
      "id": 5,
      "logged_at": "2026-08-12",
      "note": "Berhasil hari ini"
    },
    {
      "id": 4,
      "logged_at": "2026-08-11",
      "note": null
    }
  ],
  "error": null,
  "meta": {
    "page": 1,
    "per_page": 30,
    "total": 2
  }
}

4. Stats Overview — GET /stats/overview

Summary statistics for all active habits belonging to the authenticated user.

Example:

curl http://127.0.0.1:8000/api/v1/stats/overview \
  -H "Authorization: Bearer <token>" \
  -H "Accept: application/json"

Response 200:

{
  "success": true,
  "data": {
    "total_active_habits": 2,
    "completed_today": 2,
    "best_current_streak": 7,
    "habits": [
      {
        "habit_id": 1,
        "name": "Minum air 2 liter",
        "current_streak": 7,
        "longest_streak": 7,
        "completion_rate_30d": 100,
        "total_check_ins": 12,
        "checked_in_today": true
      },
      {
        "habit_id": 3,
        "name": "Olahraga pagi",
        "current_streak": 0,
        "longest_streak": 2,
        "completion_rate_30d": 40,
        "total_check_ins": 5,
        "checked_in_today": false
      }
    ]
  },
  "error": null,
  "meta": null
}

Postman Collection

A ready-to-use Postman collection is available at:

postman/Habit Tracker API - Laravel Service Repository Pattern - Yogaardiana.postman_collection.json

How to use:

  1. Open Postman → Import → select the collection file.
  2. Create an Environment Variable named token to store the token from the login response.
  3. Call Login first, then either configure a test script to store the token automatically, or fill it in manually on the Authorization tab.

Testing

Run the PHPUnit test suite:

php artisan test

⚠️ Tests currently use the MySQL database connection as configured in .env. Make sure the database is available before running the tests.


License

This project is open source and distributed under the MIT license.

About

A RESTful API for habit tracking built with Laravel 11, Laravel Sanctum token authentication, and the Service Repository Pattern architecture

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages