Files
apophis-fastify/docs/examples/crud-api.ts
T

165 lines
3.8 KiB
TypeScript

import Fastify from 'fastify'
import apophisPlugin from 'apophis-fastify'
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'error', // Validate contracts on every request
cleanup: true, // Auto-cleanup resources on exit
})
// In-memory store for demo
const users = new Map<string, { id: string; email: string; name: string }>()
// CREATE — constructor
fastify.post('/users', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'status:201',
'response_body(this).id != null',
'response_body(this).email == request_body(this).email',
],
body: {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
name: { type: 'string', minLength: 1 }
},
required: ['email', 'name']
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string' },
name: { type: 'string' }
}
}
}
}
}, async (req, reply) => {
const id = `usr-${Date.now()}`
const user = { id, email: req.body.email, name: req.body.name }
users.set(id, user)
reply.status(201)
return user
})
// READ — observer
fastify.get('/users/:id', {
schema: {
'x-category': 'observer',
'x-requires': ['users:id'],
'x-ensures': [
'status:200',
'response_body(this).id == request_params(this).id',
],
params: {
type: 'object',
properties: {
id: { type: 'string' }
},
required: ['id']
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string' },
name: { type: 'string' }
}
}
}
}
}, async (req) => {
const user = users.get(req.params.id)
if (!user) {
throw new Error('User not found')
}
return user
})
// UPDATE — mutator
fastify.put('/users/:id', {
schema: {
'x-category': 'mutator',
'x-requires': ['users:id'],
'x-ensures': [
'status:200',
'response_body(this).id == request_params(this).id',
],
params: {
type: 'object',
properties: {
id: { type: 'string' }
},
required: ['id']
},
body: {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
name: { type: 'string', minLength: 1 }
}
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string' },
name: { type: 'string' }
}
}
}
}
}, async (req) => {
const user = users.get(req.params.id)
if (!user) {
throw new Error('User not found')
}
const updated = {
...user,
email: req.body.email ?? user.email,
name: req.body.name ?? user.name,
}
users.set(req.params.id, updated)
return updated
})
// DELETE — destructor
fastify.delete('/users/:id', {
schema: {
'x-category': 'destructor',
'x-requires': ['users:id'],
'x-ensures': ['status:204'],
params: {
type: 'object',
properties: {
id: { type: 'string' }
},
required: ['id']
}
}
}, async (req, reply) => {
users.delete(req.params.id)
reply.status(204)
})
await fastify.ready()
// Run contract tests (all non-utility routes, property-based)
const result = await fastify.apophis.contract({ depth: 'standard' })
console.log('Contract tests:', result.summary)
// Run stateful tests (constructor→mutator→destructor sequences)
const stateful = await fastify.apophis.stateful({ depth: 'standard', seed: 42 })
console.log('Stateful tests:', stateful.summary)
// Validate a single route
const check = await fastify.apophis.check('POST', '/users')
console.log('POST /users check:', check.ok ? 'PASS' : 'FAIL')