# Project Api

> >

- **Type:** Skill
- **Install:** `agentstack add skill-nasrulhazim-agent-skills-project-api`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [nasrulhazim](https://agentstack.voostack.com/s/nasrulhazim)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [nasrulhazim](https://github.com/nasrulhazim)
- **Source:** https://github.com/nasrulhazim/agent-skills/tree/main/skills/project-api

## Install

```sh
agentstack add skill-nasrulhazim-agent-skills-project-api
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# API Lifecycle Manager

A seven-phase methodology for designing, building, testing, deploying, documenting, governing,
and securing production-ready REST APIs in Laravel — anchored on OpenAPI 3.1 as the single
source of truth.

## Command Reference

| Command | Phase | Description |
|---|---|---|
| `/api design` | Design | Interview + generate OpenAPI 3.1 spec |
| `/api develop` | Develop | Scaffold controllers, requests, resources from OpenAPI |
| `/api test` | Test | Generate Pest API tests + contract tests |
| `/api deploy` | Deploy | Versioned deployment, feature flags, zero-downtime |
| `/api docs` | Documentation | Auto-generate docs via Scribe/Scramble |
| `/api govern` | Governance | Breaking change detection, deprecation, review |
| `/api security` | Security | OWASP API Top 10 audit + hardening |

---

## 1. `/api design` — OpenAPI Spec Generation

### 1.1 API Discovery Interview

Ask the user for the following in **three blocks**, one at a time:

**Block 1 — API Identity**
- What is the API name and purpose? (one sentence)
- Who are the consumers? (SPA, mobile app, third-party, internal service)
- What authentication method? (Sanctum for SPA/mobile, Passport for third-party, API keys)
- What base URL / server environments? (local, staging, production)

**Block 2 — Resources & Operations**
- List the primary resources (e.g., users, orders, products)
- For each resource: which CRUD operations are needed?
- Are there any non-CRUD actions? (e.g., approve, publish, archive)
- What relationships exist between resources? (belongs-to, has-many, many-to-many)

**Block 3 — Behaviour & Constraints**
- Versioning strategy: URI-based (`/api/v1/`) or header-based (`Accept: application/vnd.app.v1+json`)?
- Pagination style: cursor-based or offset-based? Default page size?
- Filtering and sorting requirements?
- Rate limiting tiers? (public vs authenticated vs admin)
- Error response format: RFC 7807 Problem Details or custom?

If the user already provided context, extract what you can and only ask for what is missing.

### 1.2 Generate OpenAPI 3.1 Specification

Read `references/openapi-template.md` for the base template structure.

Generate a complete OpenAPI 3.1 YAML specification with:

- `info` block with title, description, version, contact, license
- `servers` for each environment
- `paths` for every resource and operation identified
- `components/schemas` for request bodies, response bodies, error objects
- `components/securitySchemes` for the chosen auth method
- `components/parameters` for reusable query parameters (pagination, filtering, sorting)
- Proper HTTP status codes: 200, 201, 204, 400, 401, 403, 404, 409, 422, 429, 500
- RFC 7807 error response schema (if selected)
- Pagination envelope schema

**Naming conventions (enforce on every path):**
- Plural nouns for collections: `/api/v1/orders` not `/api/v1/order`
- Kebab-case for multi-word: `/api/v1/order-items` not `/api/v1/orderItems`
- Nested resources max 2 levels: `/api/v1/orders/{order}/items`
- No verbs in paths: use HTTP methods instead

### 1.3 Present Design Output

Save the spec as `openapi.yaml`. Present it and ask:
"Review the spec. Want to adjust any resources, add fields, or change behaviour? Once confirmed,
I can scaffold the Laravel code with `/api develop`."

---

## 2. `/api develop` — Laravel Scaffolding from OpenAPI

### 2.1 Pre-flight Checks

Before scaffolding, verify:
- An `openapi.yaml` exists in the project (or user provides one)
- Laravel version (10 or 11) — check `composer.json`
- Auth package installed (Sanctum or Passport)

### 2.2 Generate Per-Resource Files

For each resource in the OpenAPI spec, generate:

| File | Location | Purpose |
|---|---|---|
| Controller | `app/Http/Controllers/Api/V1/{Resource}Controller.php` | RESTful actions |
| Form Request (Store) | `app/Http/Requests/Api/V1/Store{Resource}Request.php` | Create validation |
| Form Request (Update) | `app/Http/Requests/Api/V1/Update{Resource}Request.php` | Update validation |
| API Resource | `app/Http/Resources/Api/V1/{Resource}Resource.php` | Response transformation |
| Resource Collection | `app/Http/Resources/Api/V1/{Resource}Collection.php` | Paginated collection |
| Policy | `app/Policies/{Resource}Policy.php` | Authorization |
| Migration | `database/migrations/{timestamp}_create_{table}_table.php` | Database schema |
| Model | `app/Models/{Resource}.php` | Eloquent model |

### 2.3 Controller Pattern

```php
authorizeResource(Order::class, 'order');
    }

    public function index(Request $request): OrderCollection
    {
        $orders = QueryBuilder::for(Order::class)
            ->allowedFilters(['status', 'customer_id', 'created_at'])
            ->allowedSorts(['created_at', 'total', 'status'])
            ->allowedIncludes(['customer', 'items'])
            ->paginate($request->input('per_page', 15))
            ->appends($request->query());

        return new OrderCollection($orders);
    }

    public function store(StoreOrderRequest $request): JsonResponse
    {
        $order = Order::create($request->validated());

        return (new OrderResource($order))
            ->response()
            ->setStatusCode(201);
    }

    public function show(Order $order): OrderResource
    {
        return new OrderResource($order->load(['customer', 'items']));
    }

    public function update(UpdateOrderRequest $request, Order $order): OrderResource
    {
        $order->update($request->validated());

        return new OrderResource($order->fresh());
    }

    public function destroy(Order $order): JsonResponse
    {
        $order->delete();

        return response()->json(null, 204);
    }
}
```

### 2.4 Form Request Pattern

```php
 ['required', 'exists:customers,id'],
            'items' => ['required', 'array', 'min:1'],
            'items.*.product_id' => ['required', 'exists:products,id'],
            'items.*.quantity' => ['required', 'integer', 'min:1', 'max:999'],
            'items.*.unit_price' => ['required', 'numeric', 'min:0'],
            'notes' => ['nullable', 'string', 'max:1000'],
        ];
    }

    protected function failedValidation(Validator $validator): void
    {
        throw new HttpResponseException(
            response()->json([
                'type' => 'https://httpstatuses.com/422',
                'title' => 'Unprocessable Entity',
                'status' => 422,
                'detail' => 'The given data was invalid.',
                'errors' => $validator->errors()->toArray(),
            ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY)
        );
    }
}
```

### 2.5 API Resource Pattern

```php
 $this->id,
            'type' => 'orders',
            'attributes' => [
                'status' => $this->status,
                'total' => $this->total,
                'notes' => $this->notes,
                'created_at' => $this->created_at->toIso8601String(),
                'updated_at' => $this->updated_at->toIso8601String(),
            ],
            'relationships' => [
                'customer' => new CustomerResource($this->whenLoaded('customer')),
                'items' => OrderItemResource::collection($this->whenLoaded('items')),
            ],
            'links' => [
                'self' => route('api.v1.orders.show', $this->id),
            ],
        ];
    }
}
```

### 2.6 Authentication Setup

**Sanctum (SPA / First-party mobile):**

```php
// routes/api.php
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
    Route::apiResource('orders', OrderController::class);
});

// Unauthenticated routes
Route::prefix('v1')->group(function () {
    Route::get('products', [ProductController::class, 'index']);
});
```

**Passport (Third-party / OAuth):**

```php
// routes/api.php
Route::prefix('v1')->middleware('auth:api')->group(function () {
    Route::apiResource('orders', OrderController::class)
        ->middleware('scope:orders-read,orders-write');
});
```

### 2.7 Rate Limiting

```php
// app/Providers/AppServiceProvider.php (Laravel 11)
// or RouteServiceProvider (Laravel 10)

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('api', function (Request $request) {
    return match (true) {
        $request->user()?->isAdmin() => Limit::none(),
        $request->user() !== null => Limit::perMinute(120)->by($request->user()->id),
        default => Limit::perMinute(30)->by($request->ip()),
    };
});
```

### 2.8 Query Scoping

Install `spatie/laravel-query-builder` for filtering, sorting, and includes:

```php
// Scoped queries prevent users from accessing other tenants' data
$orders = QueryBuilder::for(
    Order::where('tenant_id', $request->user()->tenant_id)
)
    ->allowedFilters(['status', 'created_at'])
    ->allowedSorts(['created_at', 'total'])
    ->paginate();
```

### 2.9 Present Development Output

Present all generated files grouped by type. Ask:
"All scaffolding is generated. Run `php artisan migrate` to create tables.
Want me to generate Pest tests with `/api test`?"

---

## 3. `/api test` — Pest API Test Generation

### 3.1 Test Discovery

Read the OpenAPI spec (or scan existing controllers) to determine:
- All endpoints and HTTP methods
- Required authentication
- Validation rules from Form Requests
- Expected response structures from API Resources

### 3.2 Generate Test Files

For each resource, generate a test file at `tests/Feature/Api/V1/{Resource}Test.php`.

Read `references/api-test-patterns.md` for the complete test pattern library.

Each test file must include:

| Test Category | Tests |
|---|---|
| CRUD operations | index, store, show, update, destroy |
| Authentication | unauthenticated access returns 401, forbidden returns 403 |
| Validation | required fields, invalid types, max lengths, unique constraints |
| Error responses | 404 for missing resource, 409 for conflicts |
| Pagination | default page size, custom page size, cursor navigation |
| Filtering | valid filters, invalid filters ignored |
| Sorting | ascending, descending, default sort |
| Rate limiting | exceeding rate limit returns 429 |

### 3.3 Test Structure Pattern

```php
user = User::factory()->create();
    $this->admin = User::factory()->admin()->create();
});

describe('GET /api/v1/orders', function () {
    it('returns paginated orders for authenticated user', function () {
        Order::factory()->count(20)->for($this->user, 'customer')->create();

        actingAs($this->user, 'sanctum')
            ->getJson('/api/v1/orders')
            ->assertOk()
            ->assertJsonCount(15, 'data')
            ->assertJsonStructure([
                'data' => [['id', 'type', 'attributes' => ['status', 'total', 'created_at']]],
                'links' => ['first', 'last', 'prev', 'next'],
                'meta' => ['current_page', 'last_page', 'per_page', 'total'],
            ]);
    });

    it('returns 401 for unauthenticated request', function () {
        getJson('/api/v1/orders')
            ->assertUnauthorized();
    });

    it('filters orders by status', function () {
        Order::factory()->for($this->user, 'customer')->create(['status' => 'pending']);
        Order::factory()->for($this->user, 'customer')->create(['status' => 'completed']);

        actingAs($this->user, 'sanctum')
            ->getJson('/api/v1/orders?filter[status]=pending')
            ->assertOk()
            ->assertJsonCount(1, 'data');
    });

    it('sorts orders by created_at descending', function () {
        actingAs($this->user, 'sanctum')
            ->getJson('/api/v1/orders?sort=-created_at')
            ->assertOk();
    });
});

describe('POST /api/v1/orders', function () {
    it('creates an order with valid data', function () {
        $payload = [
            'customer_id' => $this->user->id,
            'items' => [
                ['product_id' => 1, 'quantity' => 2, 'unit_price' => 29.99],
            ],
        ];

        actingAs($this->user, 'sanctum')
            ->postJson('/api/v1/orders', $payload)
            ->assertCreated()
            ->assertJsonPath('data.type', 'orders');
    });

    it('returns 422 with validation errors for missing required fields', function () {
        actingAs($this->user, 'sanctum')
            ->postJson('/api/v1/orders', [])
            ->assertUnprocessable()
            ->assertJsonValidationErrors(['customer_id', 'items']);
    });

    it('returns 422 when items array is empty', function () {
        actingAs($this->user, 'sanctum')
            ->postJson('/api/v1/orders', [
                'customer_id' => $this->user->id,
                'items' => [],
            ])
            ->assertUnprocessable()
            ->assertJsonValidationErrors(['items']);
    });
});
```

### 3.4 Contract Testing

Generate contract tests that validate responses against the OpenAPI spec:

```php
describe('Contract: GET /api/v1/orders', function () {
    it('matches OpenAPI schema for list response', function () {
        Order::factory()->count(3)->for($this->user, 'customer')->create();

        $response = actingAs($this->user, 'sanctum')
            ->getJson('/api/v1/orders');

        $response->assertOk();

        // Validate each item matches the schema
        $data = $response->json('data');

        foreach ($data as $item) {
            expect($item)->toHaveKeys(['id', 'type', 'attributes', 'relationships', 'links']);
            expect($item['type'])->toBe('orders');
            expect($item['attributes'])->toHaveKeys(['status', 'total', 'created_at', 'updated_at']);
            expect($item['attributes']['created_at'])->toMatch('/^\d{4}-\d{2}-\d{2}T/');
        }
    });

    it('matches OpenAPI error schema for 422', function () {
        $response = actingAs($this->user, 'sanctum')
            ->postJson('/api/v1/orders', []);

        $response->assertUnprocessable();
        $error = $response->json();

        expect($error)->toHaveKeys(['type', 'title', 'status', 'detail', 'errors']);
        expect($error['status'])->toBe(422);
        expect($error['type'])->toBeString();
    });
});
```

### 3.5 Present Test Output

Present all test files. Ask:
"Run `php artisan test --filter=Api` to execute. Want me to add deployment setup with `/api deploy`?"

---

## 4. `/api deploy` — Versioned Deployment

### 4.1 API Versioning Strategy

**URI-based versioning (recommended for most Laravel apps):**

```
app/Http/Controllers/Api/
├── V1/
│   ├── OrderController.php
│   └── ProductController.php
└── V2/
    ├── OrderController.php    ← new version, extends or replaces V1
    └── ProductController.php
```

```php
// routes/api.php
Route::prefix('v1')->as('api.v1.')->group(function () {
    Route::apiResource('orders', Api\V1\OrderController::class);
});

Route::prefix('v2')->as('api.v2.')->group(function () {
    Route::apiResource('orders', Api\V2\OrderController::class);
});
```

**Header-based versioning (for advanced use cases):**

```php
// app/Http/Middleware/ApiVersion.php
class ApiVersion
{
    public function handle(Request $request, Closure $next): Response
    {
        $version = $request->header('Accept') === 'application/vnd.app.v2+json'
            ? 'v2'
            : 'v1';

        $request->attributes->set('api_version', $version);

        return $next($request);
    }
}
```

### 4.2 Feature Flags for API Endpoints

```php
// config/api-features.php
return [
    'v2_orders_endpoint' => env('API_FEATURE_V2_ORDERS', false),
    'bulk_operations' => env('API_FEATURE_BULK_OPS', false),
    'advanced_filtering' => env('API_FEATURE_ADVANCED_FILTER', true),
];

// Usage in controller
public function index(Request $request): OrderCollection
{
    $query = QueryBuilder::for(Order::class);

    if (config('api-features.advanced_filtering')) {
        $query->allowedFilters(['status', 'customer_id', 'date_range', 'total_min', 'total_max']);
    } else {
        $query->allowedFilters(['status', 'customer_id']);
    }

    return new OrderCollection($query

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [nasrulhazim](https://github.com/nasrulhazim)
- **Source:** [nasrulhazim/agent-skills](https://github.com/nasrulhazim/agent-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-nasrulhazim-agent-skills-project-api
- Seller: https://agentstack.voostack.com/s/nasrulhazim
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
