AgentStack
SKILL verified MIT Self-run

Project Api

skill-nasrulhazim-agent-skills-project-api · by nasrulhazim

>

No reviews yet
0 installs
17 views
0.0% view→install

Install

$ agentstack add skill-nasrulhazim-agent-skills-project-api

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Project Api? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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

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

 ['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

 $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):

// 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):

// 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

// 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:

// 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

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:

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
// 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):

// 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

// 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.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.