2026-03-10 00:00:00 -07:00
# Getting Started with APOPHIS
Get from install to your first behavioral bug in 10 minutes.
2026-04-30 11:34:00 -07:00
APOPHIS is inspired by [Invariant-Driven Automated Testing ](https://arxiv.org/abs/2602.23922 ) (Malhado Ribeiro, 2021): instead of only validating request and response shape, encode intended behavior as executable contracts and let the tool find violations automatically.
2026-03-10 00:00:00 -07:00
## Prerequisites
- Node.js 20.x or 22.x
- A Fastify app with `@fastify/swagger` registered
## Step 1: Install
``` bash
npm install apophis-fastify fastify @fastify/swagger
```
## Step 2: Scaffold
``` bash
apophis init --preset safe-ci
```
This creates:
- `apophis.config.js` — config with a `quick` profile
- `APOPHIS.md` — preset-specific guidance
- Package script: `npm run apophis:verify`
## Step 3: Add One Behavioral Contract
Pick one important route. Add an `x-ensures` clause that checks behavior across operations:
``` javascript
2026-04-30 11:25:30 -07:00
import crypto from 'crypto' ;
2026-03-10 00:00:00 -07:00
app . post ( '/users' , {
schema : {
'x-category' : 'constructor' ,
'x-ensures' : [
// BEHAVIORAL: Creating a user must make it retrievable
'response_code(GET /users/{response_body(this).id}) == 200'
]
}
} , async ( request , reply ) => {
const { name } = request . body ;
2026-04-30 11:25:30 -07:00
const id = ` usr- ${ crypto . createHash ( 'sha256' ) . update ( name ) . digest ( 'hex' ) . slice ( 0 , 8 ) } ` ;
2026-03-10 00:00:00 -07:00
reply . status ( 201 ) ;
return { id , name } ;
} ) ;
```
2026-04-30 11:50:39 -07:00
> **Warning:** Using `Date.now()` or `Math.random()` in handlers breaks determinism and replay. Use a stable function of the input instead. APOPHIS does not proactively detect nondeterministic handlers; it warns only when a replay diverges from the original run.
2026-04-30 11:25:30 -07:00
2026-03-10 00:00:00 -07:00
## Step 4: Run Verify
``` bash
apophis verify --profile quick --routes "POST /users"
```
## Example Failure
If your `GET /users/:id` handler has a bug (always returns 404), APOPHIS catches it:
``` text
Contract violation
POST /users
Profile: quick
Seed: 42
Expected
response_code(GET /users/{response_body(this).id}) == 200
Observed
2026-04-30 11:34:00 -07:00
GET /users/usr-7d865e returned 404
2026-03-10 00:00:00 -07:00
Why this matters
The resource created by POST /users is not retrievable.
Replay
apophis replay --artifact reports/apophis/failure-2026-04-28T12-30-22Z.json
Next
Check the create/read consistency for POST /users and GET /users/{id}.
```
## Step 5: Replay and Fix
Copy the replay command and run it:
``` bash
apophis replay --artifact reports/apophis/failure-2026-04-28T12-30-22Z.json
```
Fix the bug in your handler. Re-run verify. The failure should now pass.
## Next Steps
- Add more routes to your profile: `apophis verify --profile quick --routes "POST /users,PUT /users/:id"`
2026-04-30 11:25:30 -07:00
- Use wildcards to match route patterns: `apophis verify --routes 'POST /api/*'`
2026-03-10 00:00:00 -07:00
- Run all routes: `apophis verify --profile quick`
- Run only changed routes in CI: `apophis verify --profile ci --changed`
2026-04-30 11:25:30 -07:00
- Requires a git repository.
- Use machine-readable output in CI: `apophis verify --profile ci --format json-summary`
- Add observe mode for runtime drift detection: see [observe.md ](observe.md )
- Add qualify mode for scenario, stateful, and chaos checks: see [qualify.md ](qualify.md )
2026-03-10 00:00:00 -07:00
2026-04-30 11:50:39 -07:00
## Variants
Test the same route with different headers or content types:
``` javascript
await fastify . apophis . contract ( {
variants : [
{ name : 'json' , headers : { accept : 'application/json' } } ,
{ name : 'xml' , headers : { accept : 'application/xml' } }
]
} )
```
Or declare variants in the route schema:
``` javascript
app . get ( '/users' , {
schema : {
'x-variants' : [
{ name : 'json' , headers : { accept : 'application/json' } }
]
}
} )
```
2026-04-30 14:01:51 -07:00
## Plugin Options
When registering the APOPHIS plugin, you can pass these options:
``` javascript
await fastify . register ( apophis , {
// Swagger config passthrough (if @fastify/swagger is not already registered)
swagger : { openapi : { info : { title : 'API' , version : '1.0.0' } } } ,
// Runtime contract validation hooks: 'off', 'warn', or 'error'
// Only active in non-production environments
runtime : 'warn' ,
// Automatically clean up tracked resources after tests
cleanup : true ,
// Global timeout in milliseconds for all requests
timeout : 5000 ,
// Tenant isolation scopes
scopes : {
tenant1 : { headers : { 'x-tenant-id' : '1' } } ,
tenant2 : { headers : { 'x-tenant-id' : '2' } } ,
} ,
// Auth and protocol extensions
extensions : [ jwtAuth , apiKeyAuth ] ,
// Plugin hook-phase contracts
pluginContracts : {
'rate-limit' : { appliesTo : 'POST /users' , ensures : [ 'status != 429' ] } ,
} ,
// Outbound dependency contracts
outboundContracts : {
'payment-api' : {
target : 'https://payments.example.com' ,
method : 'POST' ,
response : { 200 : { type : 'object' , properties : { id : { type : 'string' } } } }
}
}
} )
```
## Schema Annotations
APOPHIS reads these OpenAPI schema extensions:
| Annotation | Location | Description |
|---|---|---|
| `x-category` | Top-level | Route classification: `constructor` , `mutator` , `observer` , `destructor` , `utility` |
| `x-ensures` | Top-level or `response[statusCode]` | Post-condition contracts (APOSTL formulas) |
| `x-requires` | Top-level or `response[statusCode]` | Pre-condition contracts (APOSTL formulas) |
| `x-variants` | Top-level | Request variants for content-type negotiation or feature flags |
| `x-timeout` | Top-level or `response[statusCode]` | Per-route timeout in milliseconds |
| `x-outbound` | Top-level | Outbound dependency contracts for this route |
| `x-streaming` | Top-level | Mark route as streaming (populates `chunks` and `streamDurationMs` in eval context) |
| `x-validate-runtime` | Top-level or `response[statusCode]` | Toggle runtime validation for this route (default: true) |
| `x-extension-config` | Top-level | Per-route config for extensions (e.g., `{ jwt: { verify: false } }` ) |
Annotations can be placed on the top-level schema or nested inside `response[statusCode]` . Nested annotations take precedence for that status code.
## Programmatic API
After registration, `fastify.apophis` provides:
``` javascript
// Run contract tests for all routes
const suite = await fastify . apophis . contract ( { runs : 50 , seed : 42 } )
// Run stateful tests
const stateful = await fastify . apophis . stateful ( { runs : 50 , seed : 42 } )
// Run a single scenario
const scenario = await fastify . apophis . scenario ( {
name : 'oauth-basic' ,
steps : [ ... ]
} )
// Check a single route
const result = await fastify . apophis . check ( 'GET' , '/users/:id' )
// Get enriched OpenAPI spec with contract metadata
const spec = fastify . apophis . spec ( )
// Clean up tracked resources
await fastify . apophis . cleanup ( )
// Test-only utilities (NODE_ENV=test only)
fastify . apophis . test . registerPluginContracts ( 'name' , spec )
fastify . apophis . test . registerOutboundContracts ( { ... } )
fastify . apophis . test . enableOutboundMocks ( { mode : 'example' } )
fastify . apophis . test . disableOutboundMocks ( )
const calls = fastify . apophis . test . getOutboundCalls ( 'payment-api' )
```
2026-03-10 00:00:00 -07:00
## Config Reference
2026-04-30 11:25:30 -07:00
For the full configuration reference, see [CLI Reference ](cli.md ).
2026-03-10 00:00:00 -07:00
## Monorepo Workspaces
2026-04-30 11:25:30 -07:00
Use `--workspace` to run verify or doctor across all packages:
2026-03-10 00:00:00 -07:00
``` bash
apophis verify --workspace --profile quick --format json
```
2026-04-30 11:25:30 -07:00
See [CLI Reference ](cli.md ) for workspace output format and exit codes.