ssdev/laravel-api-contracts
Composer 安装命令:
composer require ssdev/laravel-api-contracts
包简介
API response contract snapshot testing for Laravel — detects breaking changes before they reach production.
README 文档
README
API response contract snapshot testing for Laravel — catch breaking changes before they hit production.
How it works
- You write contract tests that hit your API endpoints and call
assertMatchesApiContract() - On first run, the response shape is saved as a JSON snapshot (committed to git)
- On every subsequent
git push, the hook re-runs those tests and compares against the snapshots - If a field was removed or a type changed, the push is blocked — you see exactly what broke
- New fields (additive changes) are reported but never block a push
Snapshots are plain JSON files committed alongside your code. Any contract change is a visible git diff.
Installation
composer require ssdev/laravel-api-contracts --dev
php artisan api:contract:install
This sets up:
tests/snapshots/api/— snapshot directory (commit this).githooks/pre-commit— warns about routes with no snapshot coverage.githooks/pre-push— blocks push if existing contracts are brokengit config core.hooksPath .githooks
Generating test stubs
Scan your API routes and generate a test file automatically:
php artisan api:contract:generate --prefix=api/v1
This produces tests/Feature/ApiContractTest.php with one test per GET route, ready to fill in:
<?php use Ssdev\ApiContracts\Testing\InteractsWithApiContract; uses(InteractsWithApiContract::class); uses(Illuminate\Foundation\Testing\RefreshDatabase::class); // TODO: fill in your auth headers function contractHeaders(): array { return ['X-API-KEY' => '...', 'Accept' => 'application/json']; } // --------------------------------------------------------------------------- // GET /api/v1/products // --------------------------------------------------------------------------- it('GET /api/v1/products matches contract', function () { $response = $this->withHeaders(contractHeaders())->getJson('/api/v1/products'); $response->assertStatus(200); $this->assertMatchesApiContract('GET_api_v1_products', $response->json()); }); // --------------------------------------------------------------------------- // GET /api/v1/products/{id} // --------------------------------------------------------------------------- it('GET /api/v1/products/{id} matches contract', function () { $id = 1; // TODO: replace with a valid id $response = $this->withHeaders(contractHeaders())->getJson("/api/v1/products/{$id}"); $response->assertStatus(200); $this->assertMatchesApiContract('GET_api_v1_products_show', $response->json()); });
Non-GET routes are generated as commented-out stubs for you to implement manually.
After filling in the auth setup, generate the initial snapshots:
php artisan api:contract:update
Adding tests for new routes
When you add new routes later, run with --merge to append only the missing tests without touching existing ones:
php artisan api:contract:generate --merge
Git hooks
pre-commit — coverage check
Every commit checks whether any API routes are missing snapshot coverage and warns you:
[api:contract] Coverage warning:
→ POST /api/v1/orders
→ DELETE /api/v1/orders/{id}
Run php artisan api:contract:generate --merge to add test stubs.
(this is a warning only — commit is not blocked)
pre-push — contract enforcement
Every push runs your contract tests. If a breaking violation is detected, the push is blocked:
Running API contract tests...
[GET_api_v1_products] BREAKING API contract violation:
✖ [BREAKING] Field removed: 'data.products[].sku' (was 'string')
✖ [BREAKING] Type changed: 'data.products[].price' was 'integer', now 'string'
+ [NEW] New field: 'data.products[].discount'
If INTENTIONAL: run php artisan api:contract:update
then commit the snapshot files and push again.
If ACCIDENTAL: fix the code before pushing.
Accept these changes and update snapshots now? (y/N)
If you answer y, snapshots are regenerated in place. You commit them and push again — the updated contract becomes part of the same push.
Violations
| Type | Meaning | Blocks push? |
|---|---|---|
REMOVED |
Field existed in snapshot, now missing | Yes |
TYPE_CHANGED |
Field type changed (e.g. integer → string) |
Yes |
NEW |
Field added, not in snapshot | No |
A null value in a snapshot is treated as "unknown type" — if the field later returns a real value, it is not flagged as a type change.
Commands
| Command | Description |
|---|---|
api:contract:install |
Install hooks, snapshot dir, git config |
api:contract:generate --prefix=api/v1 |
Generate test stubs from routes |
api:contract:generate --merge |
Add tests for new routes only |
api:contract:update |
Regenerate all snapshots |
api:contract:coverage --prefix=api/v1 |
Report routes with no snapshot coverage |
Writing tests manually
If you prefer to write tests by hand, use the InteractsWithApiContract trait directly:
Pest:
use Ssdev\ApiContracts\Testing\InteractsWithApiContract; uses(InteractsWithApiContract::class); it('products index matches contract', function () { $response = $this->getJson('/api/v1/products'); $response->assertStatus(200); $this->assertMatchesApiContract('GET_products', $response->json()); });
PHPUnit:
use Ssdev\ApiContracts\Testing\InteractsWithApiContract; class ApiContractTest extends TestCase { use InteractsWithApiContract; public function test_products_index(): void { $response = $this->getJson('/api/v1/products'); $this->assertMatchesApiContract('GET_products', $response->json()); } }
Snapshot format
Snapshots capture the type shape of your response, not the actual values:
{
"success": "boolean",
"data": {
"products": [
{
"id": "integer",
"name": "string",
"price": "double",
"status": "string",
"brand": {
"id": "integer",
"name": "string",
"logo_url": "null"
}
}
],
"pagination": {
"current_page": "integer",
"per_page": "integer",
"total": "integer",
"last_page": "integer"
}
},
"message": "string"
}
Arrays are represented by the shape of their first element. Committing these files gives you a permanent, reviewable record of your API contract — any change is visible as a git diff.
Configuration
php artisan vendor:publish --tag=api-contract-config
// config/api-contract.php return [ 'snapshot_dir' => 'tests/snapshots/api', // where snapshots are stored 'test_path' => 'tests/Feature/ApiContractTest.php', // used by update + hook 'test_flags' => '--no-coverage', // extra flags for test runner 'update_env' => 'API_CONTRACT_UPDATE', // env var that triggers snapshot write 'route_prefix' => 'api', // prefix for generate + coverage commands 'hooks_dir' => '.githooks', // where hooks are installed ];
Requirements
- PHP 8.2+
- Laravel 10, 11, or 12
License
MIT
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 0
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-01