# build-api — Versioned API endpoint builder

> **Builds:** one versioned API endpoint on top of an already-generated model — **route** (`routes/api/v1/*.php`) + **controller** (`app/Http/Controllers/Api/V1/*`) + **service** (`app/Services/<Area>/*`) + **form request** (`app/Http/Requests/Api/<Module>/*`) + **API resource/collection** (`app/Http/Resources/Api/V1/*`), all returning the base standard JSON envelope.
> **Source of truth (no duplication):**
> - Model / translation model / factory / seeder: [`build-database.md`](build-database.md) — this builder assumes those exist.
> - Postman collection + examples + API docs generation: [`build-postman-collection.md`](build-postman-collection.md) — this builder stops at the implementation layer.
> - Column → type/Faker/JSON shape: [`../schema-json-contract.md`](../schema-json-contract.md)
> - This file owns **only** the API implementation layer (route → request → controller → service → resource → response envelope).
> **Consumed by:** the per-API plan (runs [`build-database.md`](build-database.md) first, then this), and the setup pipeline.

**Inputs:** an existing model (with `RELATIONS`, `$translatedAttributes`, casts per [`build-database.md`](build-database.md)); the endpoint's purpose, HTTP method, URL segments, auth requirement, and the response fields the frontend needs. User-facing strings come from `lang/*` (`__('api/...')`); never hardcode English in returned messages.

> These three `.cursor` API skills — `api-design.md`, `api-end-to-end-execution.md`, `create-api-with-postman.md` — overlap heavily and are generic (no base specifics). They have been **consolidated** here (implementation half) and into [`build-postman-collection.md`](build-postman-collection.md) (examples/docs/Postman half). Follow this file, not those skills, for base-exact conventions.

---

## API correctness — hard rules (learned from real projects)

Apply ALL — each maps to a mistake seen in generated code. Most just re-assert base's own conventions.

**Validation (Form Request):**
1. **No client-only validations.** Don't validate UI-only concerns on the server (`'terms' => ['required','accepted']`, confirm-password match, …) — that's the mobile/client's job.
2. **Enum-backed fields validate via the enum:** `Rule::enum(<Enum>::class)` (or `Rule::in(<Enum>::values())`) — never a literal `in:normal,business`.
3. **SoftDeletes-aware `unique`/`exists`:** on a soft-deletable table use `Rule::unique('users','email')->whereNull('deleted_at')` — a trashed row must not block re-use.
4. **Phone intake:** `country_code` + `phone` are **two separate fields**; validate `country_code` with `exists:countries,code`; normalize the phone (strip a leading `+` and `0`) via `PhoneNormalizer` in `prepareForValidation`. (Mirror base `UserResource` — it already emits `country_code` + `normalized_phone`.)
5. **Files are normal inputs.** Validate the file (`image`/`file` rules) and pass it through `$data`; the model's `FILES` + `app/Traits/Upload/BaseFilesTrait` uploads it on create/update. **No** `UploadedFile` service param, **no** manual `$model->image = ...; $model->save();`.

**Resource / response shape:**
6. **Single resource → flat `data`.** Return the resource directly as `data` (`"data": { …fields }`) — no redundant `data.user` wrapper. Name sub-keys only for genuine multi-part payloads.
7. **Auth token lives INSIDE the Resource** (base `UserResource` — at `app/Http/Resources/Auth/UserResource.php` — takes it via the constructor and emits a `token` field) — never a hand-assembled `data.token` sibling.
8. **Enums serialize as `{value, label}`** via `App\Traits\Enums\GeneralEnumTrait::toValueLabel()` (the base trait every enum already uses; the enum sets `const PATH` for its translated `label()`), not the raw value.
9. **Include computed/derived fields** the frontend needs (e.g. `is_subscribed`, `has_used_free_plan`, `trusted_access: {enabled, inactivity_days, trusted_user}`) — derive from relations/state; don't ship raw columns only. (Model side of this in `build-database.md`; validation side in #3.)

**Errors & performance:**
10. **Never leak exception internals to clients.** In the controller `try/catch`, log server-side (`report($e)`) and return a **generic localized** message via `respondWithFail(__('<a generic api key>'), [], 500)` (add the key to `lang/{ar,en}/api/messages.php`) — do **NOT** echo `$e->getMessage()` in a production response (it exposes SQL/paths). Let validation/auth exceptions propagate to the `bootstrap/app.php` handler instead of catching them.
11. **Eager-load what the Resource reads (no N+1).** The service `->with([...])` every relation the Resource surfaces via `whenLoaded`; never trigger a lazy load inside a Resource; use `withCount` for counts. Recommended guardrail: enable `Model::preventLazyLoading(! app()->isProduction())` in `AppServiceProvider::boot()` so an un-eager-loaded relation throws in dev.
12. **Whitelist list filters/sorts** — expose only the fields a screen actually filters/sorts by (via `FilterableTrait` + `scope*`), never `where($request->all())` / an open `sort` column (see §3).

---

## 0) Prerequisite — model exists

Do not start until [`build-database.md`](build-database.md) has produced the model + migration + seeder and `migrate:fresh --seed` runs clean. The endpoint reads/writes through that model only.

---

## 1) Route — `routes/api/v1/<topic>.php`

API routes are **auto-mounted**: `bootstrap/app.php` discovers every `routes/api/v{N}/*.php`, wraps them under prefix `api`, applies the **`api.lang`** middleware alias, then nests each version under its own prefix (`v1`, `v2`, …). **A new file under `routes/api/v1/` is picked up automatically — never edit `bootstrap/app.php` to register routes.**

Final URL pattern: **`/api/v1/<segments>`**.

Group related URLs with `Route::prefix(...)->group(...)` inside the file. Import the controller via its FQCN. Base example (`routes/api/v1/countries.php`, `auth.php`):

```php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\<Entity>Controller;

Route::prefix('<resource>')->group(function () {
    Route::get('/', [<Entity>Controller::class, 'index']);
    Route::get('/{id}', [<Entity>Controller::class, 'show']);

    // Authenticated routes — wrap in auth:sanctum (see auth.php)
    Route::middleware('auth:sanctum')->group(function () {
        Route::post('/', [<Entity>Controller::class, 'store']);
    });
});
```

**Auth (base-exact):**
- Protected endpoints go inside `Route::middleware('auth:sanctum')->group(...)` (see `routes/api/v1/auth.php` lines 32–35). API auth is **Laravel Sanctum** (per `04-backend-rules.mdc`).
- `api.lang` is applied globally to all `api/*` by `bootstrap/app.php` — do **not** re-add it per route.
- Additional aliases available: `complete.info` (`CheckCompleteInfo`), `throttle:request-code`. Admin-only API routes use `auth:admin`.
- The token is issued in the service with `$user->createToken('authToken')->plainTextToken` and revoked with `$request->user()->currentAccessToken()->delete()` (see `AuthService`, `LogoutController`). For **multi-audience** projects the default is a **model + Sanctum guard per audience** (`auth:<audience>`, mirroring `admins`) — see [`build-auth-audience.md`](build-auth-audience.md).
- **Throttle sensitive endpoints.** Auth / OTP / code-request / payment / write-burst endpoints declare a **named limiter** — base defines `admin.login` + `request-code` in `AppServiceProvider::boot()` and applies `throttle:request-code`. Register a new limiter there and apply `throttle:<name>`; never leave an auth/OTP endpoint unlimited.
- **OTP / SMS** goes through `app/Services/Otp/OtpService.php`; delivery uses the SMS driver in `config/services.php` (default `log` → `LogCodeSender`). Swapping to a real provider (twilio/vonage) is **config, not code** — set the driver + credentials in `.env`.
- **Credential-change flows are MULTI-STEP — build every step.** Changing a phone/email that the design gates behind confirming the **CURRENT** one is **verify-current → set-new → verify-new**, one endpoint per step — never a single "change". Use the base's ready types: `OtpType::OLD_PHONE_VERIFY` then `NEW_PHONE_VERIFY` (same for email). Canonical: `send-current-code` → `verify-current-code` → `set-new-phone` (sends the new code) → `verify-new-phone` (commits). Mirror the **exact** screen sequence in the Figma flow; don't emit only the "new phone" half (a real bug seen in a generated project).

**Guest mode — decide from the design/analysis (don't hardcode one behavior):**
- **Browse-only guest** (the guest button just enters the app / home, no server action) → **no backend**: it's a client-side navigation; don't build a guest endpoint/token.
- **Action-capable guest** (the project lets a guest add to cart, favorites, etc.) → anchor guest data to a **device identifier** (the project's `mac_address` field: a **14-char alphanumeric** key the client generates and sends with each guest request). Persist guest cart/favorites/etc. against that identifier, and **merge them onto the user on login/register**. Add the column where guest-ownable rows live (e.g. `carts.mac_address`) + an index.
- Which one applies is an **analysis decision** — check whether any screen lets a guest perform a write action; record it in `analysis.md` + the `guest` plan.

---

## 2) Form Request — `app/Http/Requests/Api/<Module>/<Action>Request.php`

- Namespace: `App\Http\Requests\Api\<Module>` (e.g. `App\Http\Requests\Api\Auth`).
- **Extend `App\Http\Requests\Api\BaseApiRequest`** (NOT `FormRequest` directly). `BaseApiRequest`:
  - sets `authorize(): bool => true` (route middleware does the gating),
  - uses `ValidationResponseTrait`,
  - runs `prepareForValidation()` → `normalizeCommonInputs()` (phone/login normalization via `PhoneNormalizer`) + `normalizeInputs()` (override hook for per-module merging),
  - and — critically — its `ValidationResponseTrait` makes failed validation throw a `ValidationException` that `bootstrap/app.php` catches and renders as the standard validation envelope (see §6). **Do not** add a custom `failedValidation()` / JSON response in the request.
- **Validation messages live in `lang/{ar,en}/validation.php` — NOT in per-request `messages()`.** Use Laravel's standard mechanism:
  - the rule messages (`required`, `exists`, `unique`, …) already exist (translated) in `validation.php`;
  - translate **field names** once in `validation.php` → `'attributes' => ['phone' => 'رقم الجوال', ...]` so `:attribute` renders per locale;
  - field-specific overrides go in `validation.php` → `'custom' => ['phone' => ['exists' => '...']]`.
  So a `FormRequest` normally needs **no `messages()` and no `attributes()`** at all. **Anti-pattern (reject) — BOTH of these:** `messages()` returning `'phone.exists' => __('api/auth.exists', [...])`, **and** `attributes()` returning `['phone' => __('api/auth.phone'), 'code' => __('api/auth.code')]`. Move field names to `validation.php` → `attributes` (`phone`, `code`, …) and rely on the standard rule messages / `custom.<field>.<rule>`. The Form Request keeps only `rules()`.

```php
<?php

namespace App\Http\Requests\Api\<Module>;

use App\Http\Requests\Api\BaseApiRequest;

class <Action>Request extends BaseApiRequest
{
    public function rules(): array
    {
        return [
            // 'field' => ['required', 'string', 'max:255'],
        ];
    }

    // No messages()/attributes() — messages + translated field names live in lang/{ar,en}/validation.php.
    // Override only when extra sanitization is needed:
    // protected function normalizeInputs(): void { $this->merge([...]); }
}
```

Add the field names to **both** `lang/ar/validation.php` and `lang/en/validation.php` under `attributes` (and any `custom` overrides). Derive rules from real columns only — never invent fields. Read-only `GET` endpoints typically take a plain `Request` (no form request) — see `CountriesController`.

---

## 3) Service — `app/Services/<Area>/<Entity>Service.php`

All business logic / queries live here; the controller stays thin (`04-backend-rules.mdc`).

- Plain PHP class; inject dependencies via the constructor.
- Accept the `Request` for list/filter/pagination methods, or a **validated array** (`$request->validated()`) / IDs for writes.
- Wrap multi-step writes in `DB::transaction(...)`.
- For reads, base uses the fluent `App\Services\HelperQueries\BaseQueryService` wrapper (see `CountryService`): `resetQuery()->whereArray([...])->select([...])->with([...])->orderByIdDesc()->paginate($request)` (or `->limit($request)` for non-paginated, `->find($id)`/`->first()`). Direct Eloquent is also used when eager-load shaping is needed.
- Return type matters: return a `LengthAwarePaginator` for paginated lists (the controller wraps it in a **Collection resource**), a plain `Collection` for limited lists, or a model for single records.

```php
<?php

namespace App\Services\<Area>;

use App\Models\<Entity>;
use App\Services\HelperQueries\BaseQueryService;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;

class <Entity>Service
{
    protected BaseQueryService $queryService;

    public function __construct()
    {
        $this->queryService = new BaseQueryService(new <Entity>());
    }

    public function getList(Request $request): LengthAwarePaginator
    {
        return $this->queryService
            ->resetQuery()
            ->whereArray(['is_active' => true])
            ->select(['id', /* real columns */])
            ->orderByIdDesc()
            ->paginate($request);
    }

    public function store(array $data): <Entity>
    {
        return DB::transaction(fn () => <Entity>::create($data));
    }
}
```

### Keep it SIMPLE — Laravel-native, minimal code (mandatory)
The generated code must be the **least code that works**, leaning on the framework — not hand-assembled:
- **Create/update from validated data:** `<Entity>::create($data)` where `$data = $request->validated()`, adding **only** the few genuinely computed/derived fields — e.g. `$data + ['user_code' => ...]`. **Never hand-list every column**, and **never pass values that are already defaulted** (`is_active`, `is_blocked`, `is_notify`, `user_type`, `user_code => null`…). (Anti-pattern seen: `User::create([...12 fields incl. defaults...])`.)
- **Defaults live in the MODEL (preferred), not the migration.** Set defaults via the model `protected $attributes = [...]` (and casts / enum casts) so they're visible and consistent at the model layer; use migration `default()` only as a DB safety net, not the primary source.
- **Derived values belong in the MODEL, not ad-hoc service helpers.** Phone normalization, slugs, hashing, etc. → a model **mutator / cast / accessor** or an **Observer** (`creating`/`saving`) — never a `applyParsedPhone()`-style helper wired through the service. Remove needless indirection.
- **Clean control flow — senior-level.** Prefer `match(true)`/`match($x)` and early returns over long `if`/`if`/`if` chains; small well-named methods; PHP 8 idioms (match, enums, constructor promotion, `readonly`). The code must read like a 15-year software engineer wrote it.
- **Prefer Eloquent idioms:** mass assignment, casts, relations, scopes, `firstOrCreate`/`updateOrCreate`, `$model->update($data)` — over manual field-by-field assembly. No over-engineering; simplest path first.

### List filtering / sorting / pagination (base-exact)
- **Pagination:** `->paginate($request)` (base `BaseQueryService`) reads `page` + `per_page` — never hand-roll a pager.
- **Filtering (whitelisted):** base resolves client filters through `App\Traits\Filters\FilterableTrait` for real columns, custom `scope*` methods for derived/translated columns (e.g. `scopeTitle` via `whereTranslationLike`), and `App\Traits\Filters\DateFilterableTrait` for date ranges. Expose **only** the fields a screen filters by — never `where($request->all())`.
- **Sorting:** default `->orderByIdDesc()`; accept an explicit `sort`/`order` only against a **whitelisted** column set.
- **Eager-load** every relation the Resource reads (`->with([...])`, `withCount`) — no N+1 (rule #11).

---

## 4) Controller — `app/Http/Controllers/Api/V1/<Entity>Controller.php`

- **Group a flow into a subfolder with FOCUSED controllers — do NOT build one fat controller.** A multi-endpoint flow (auth, …) lives in `app/Http/Controllers/Api/V1/<Flow>/` with **one focused controller per action/concern**, exactly like base's `Api/V1/Auth/`: `LoginCodeController`, `RequestCodeController`, `LoginWithPasswordController`, `MeController`, `LogoutController`. Each controller injects **only the service(s) it actually uses** (often one). **Anti-pattern (reject):** a single `AuthController` with 7 injected services and every action inside it.
- Single-entity CRUD stays a single `Api/V1/<Entity>Controller`; a flow gets the subfolder above.
- Namespace matches the path (`App\Http\Controllers\Api\V1\<Flow>`); **extend `App\Http\Controllers\Controller`** (an empty abstract base — no shared logic).
- Inject only the needed service(s) via the constructor.
- **Response traits (base-exact):** for list/CRUD controllers use `SuccessResponseTrait, FailResponseTrait, PaginationTrait` (see `CountriesController`, `CityController`). For auth/state-heavy controllers use the aggregator `App\Traits\Response\ResponseTrait` (pulls in `AuthResponseTrait`, `SystemErrorResponseTrait`, `ValidationResponseTrait`, `UserStateResponseTrait`, `SuccessResponseTrait`, `FailResponseTrait`) — see `LoginWithPasswordController`, `MeController`, `LogoutController`.
- Return `$this->respondWithSuccess($message, $data)` / `$this->respondWithFail($message, $data, $statusCode)`.
- **Wrap output in a Resource** (`new <Entity>Resource($model)` for one record; `new <Entity>Collection($paginator)` for paginated lists). Existing base controllers call `->toArray($request)` on the collection before passing it to `respondWithSuccess`.
- Base list controllers use `try/catch` returning `respondWithFail(..., 500)`; let validation/auth exceptions propagate to the `bootstrap/app.php` handler (see §6) rather than catching them.

```php
<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\<Module>\<Action>Request;
use App\Http\Resources\Api\V1\<Entity>Collection;
use App\Http\Resources\Api\V1\<Entity>Resource;
use App\Services\<Area>\<Entity>Service;
use App\Traits\Response\SuccessResponseTrait;
use App\Traits\Response\FailResponseTrait;
use App\Traits\Response\PaginationTrait;
use Illuminate\Http\Request;

class <Entity>Controller extends Controller
{
    use SuccessResponseTrait, FailResponseTrait, PaginationTrait;

    public function __construct(protected <Entity>Service $<entity>Service) {}

    public function index(Request $request)
    {
        try {
            $items = $this-><entity>Service->getList($request);

            return $this->respondWithSuccess(
                __('api/<module>.retrieved_successfully'),
                (new <Entity>Collection($items))->toArray($request)
            );
        } catch (\Exception $e) {
            return $this->respondWithFail('Failed to retrieve <resource>: ' . $e->getMessage(), [], 500);
        }
    }

    public function store(<Action>Request $request)
    {
        $model = $this-><entity>Service->store($request->validated());

        return $this->respondWithSuccess(
            __('api/<module>.created'),
            (new <Entity>Resource($model))->resolve()
        );
    }
}
```

---

## 5) API Resource & Collection — `app/Http/Resources/Api/V1/`

**Item resource** — extends `Illuminate\Http\Resources\Json\JsonResource`; map only the fields the client needs. Translatable attributes (e.g. `name`) resolve to the request locale automatically (set by `api.lang`). Use `whenLoaded(...)` for relations so they appear only when eager-loaded (mirror `CountryResource`, `CityWithRegionsResource`):

```php
<?php

namespace App\Http\Resources\Api\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class <Entity>Resource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'   => $this->id,
            'name' => $this->name, // translated automatically
            'relation' => $this->whenLoaded('relation', fn () => RelationResource::collection($this->relation)),
        ];
    }
}
```

**Collection** (paginated lists) — extends `ResourceCollection`, uses `PaginationTrait`, and emits a `data` + `pagination` shape (exactly `CountryCollection`/`CityCollection`):

```php
<?php

namespace App\Http\Resources\Api\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use App\Traits\Response\PaginationTrait;

class <Entity>Collection extends ResourceCollection
{
    use PaginationTrait;

    public function toArray(Request $request): array
    {
        return [
            'data'       => <Entity>Resource::collection($this->collection),
            'pagination' => $this->formatPaginationData($this->resource),
        ];
    }
}
```

A custom resource constructor is allowed when extra data must ride along (e.g. `UserResource($user, $token)` injecting the Sanctum token). **Auth resources live under `app/Http/Resources/Auth/`** (base `UserResource.php`); entity/CRUD resources live under `app/Http/Resources/Api/V1/` — mirror the matching one.

---

## 6) Response envelope (base-exact) — what every endpoint MUST return

All responses go through `BaseResponseTrait::mainRespond($data, $statusCode, $headers)` → `response()->json(...)`. There is **no `status`/`code` wrapper key** — the envelope is flat with a top-level `message`.

**Success** (`SuccessResponseTrait::respondWithSuccess`, HTTP **200**):
```json
{ "message": "Countries retrieved successfully", "data": { } }
```
`message` defaults to `__('Success')` when null. `data` is whatever the controller passed (resource array, or `{ "data": [...], "pagination": {...} }` from a Collection).

> **Messages mirror the design.** When a screen shows specific success / toast / inline-error copy (e.g. a login "تم تسجيل الدخول بنجاح" or a field error), use **that exact wording** as the `lang/{ar,en}/api/<file>.php` message — captured in `analysis.md`. Don't substitute a generic "done successfully"; keep both locales in sync.

**Paginated `data`** (`PaginationTrait::formatPaginationData`) — exact keys:
```json
{
  "data": [ /* resource items */ ],
  "pagination": {
    "current_page": 1, "per_page": 30, "total": 100,
    "last_page": 4, "from": 1, "to": 30
  }
}
```

**Fail** (`FailResponseTrait::respondWithFail`, default HTTP **500**): `{ "message": "..." }`, plus a `data` key only when non-empty `$data` is passed. Base list controllers return this on caught exceptions.

**Validation error** (HTTP **422**) — produced two ways, both via `ValidationResponseTrait::renderValidationErrors`:
- A `ValidationException` thrown by a `BaseApiRequest` is caught in `bootstrap/app.php` (`$request->is('api/*')` branch) and rendered.
- Or thrown explicitly via `respondValidationErrors([...])` from a controller using `ResponseTrait` (see `LoginWithPasswordController`).
```json
{ "message": "validation.invalid_data", "errors": { "field": ["..."] } }
```
(`message` defaults to `__('validation.invalid_data')`.)

**Auth / state responses** (only on controllers using `ResponseTrait`):
- `respondUnauthorized` → 400, `respondUnauthenticated` → 401, `respondForbidden` → 403 (`AuthResponseTrait`, default messages `__('response.*')`).
- `respondBlocked` → 423, `respondNotActive($msg, $user)` → 203 with `{ "token": "..." }` (`UserStateResponseTrait`).

**Generic exceptions** (`bootstrap/app.php` `api/*` branch): non-validation `Throwable` returns `{ "message": "<exception message>" }` with the mapped status (HttpException → its code, `AuthenticationException` → 401, else 500) and header `Content-Language: ar`.

**Localization:** `ApiLangMiddleware` resolves locale from (in order) `Accept-Language` header → `X-Language` header → body `lang` → query `lang` → default **`ar`**; supported: `ar`, `en`, `fr`, `es`. It calls `app()->setLocale($lang)` before the route runs, so `__()` strings and translated model attributes come back in the requested language.

---

## 7) Verify (definition of done)

1. **Endpoint reachable & envelope:** hit `GET /api/v1/<resource>` → HTTP 200, body has top-level `message` (string) + `data`. For a paginated list, `data.data` is an array and `data.pagination` has exactly `current_page, per_page, total, last_page, from, to`.
2. **Validation shape:** POST with a missing required field → HTTP **422**, body `{ "message": ..., "errors": { "<field>": [ ... ] } }` (no `data`). Confirm it routed through the form request / `bootstrap/app.php` handler, not a hand-rolled response.
3. **Localized messages:** send `Accept-Language: en` then `ar` (or `?lang=en` / `?lang=ar`) → `message` and translated attributes (e.g. `name`) change accordingly; default with no header is `ar`.
4. **Auth:** call a `auth:sanctum` route without a token → 401 from the framework/handler; with a valid `Bearer` token → 200. Logout revokes the current token.
5. **Resource fidelity:** returned fields match the resource map only (no leaked columns); relations appear only when eager-loaded (`whenLoaded`).
6. **Thin controller:** no business logic in the controller — queries/writes live in the service; writes use `DB::transaction`.

---

## References (single source — do not duplicate here)
- Model / factory / seeder this endpoint sits on → [`build-database.md`](build-database.md)
- Postman collection, response examples, and generated API docs → [`build-postman-collection.md`](build-postman-collection.md)
- Column → type / Faker / JSON shape → [`../schema-json-contract.md`](../schema-json-contract.md)
