Install
$ agentstack add skill-nasrulhazim-agent-skills-project-api ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
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:
infoblock with title, description, version, contact, licenseserversfor each environmentpathsfor every resource and operation identifiedcomponents/schemasfor request bodies, response bodies, error objectscomponents/securitySchemesfor the chosen auth methodcomponents/parametersfor 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/ordersnot/api/v1/order - Kebab-case for multi-word:
/api/v1/order-itemsnot/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.yamlexists 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.
Write a review
Versions
- v0.1.0 Imported from the upstream source.