API Producer Guide
This guide is for developers who want to build Domain APIs - whether you’re creating a new API, adding endpoints to an existing API, or implementing the backend services.
Table of Contents
- Quick Start
- API Design Principles
- Creating a New API
- Resource Structure
- Defining Relationships
- Using Shared Components
- OpenAPI Best Practices
- Testing Your API
- Implementation Guidelines
Quick Start
Prerequisites
- Node.js 18 or later
- Understanding of REST APIs and OpenAPI 3.0
- Familiarity with JSON and HTTP
Project Structure
domain-apis/
├── specs/
│ ├── shared/
│ │ └── shared-components.yaml # Shared types and parameters
│ ├── taxpayer/
│ │ └── taxpayer-api.yaml # Taxpayer API spec
│ ├── income-tax/
│ │ └── income-tax-api.yaml # Income Tax API spec
│ └── payment/
│ └── payment-api.yaml # Payment API spec
├── tests/
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ └── acceptance/ # Acceptance tests
└── docs/ # Generated documentation
Your First API Endpoint
- Define the resource schema in your OpenAPI spec
- Add relationship links to related resources
- Validate the specification
- Test with mock server
- Implement the backend
API Design Principles
1. Domain Boundaries
Each API represents a distinct domain with clear responsibilities:
- Taxpayer API: Identity and registration
- Income Tax API: Tax returns and assessments
- Payment API: Payments and allocations
Rule: Don’t mix concerns across domains. If you need data from another domain, use relationships.
2. Resource-Oriented Design
Design around resources (nouns), not actions (verbs):
✅ Good:
GET /taxpayers/{id}
POST /taxpayers
GET /tax-returns/{id}
❌ Avoid:
GET /getTaxpayer?id=123
POST /createTaxpayer
GET /fetchTaxReturn?id=456
3. Hypermedia Links
Every resource MUST include _links to related resources:
{
"id": "TP123456",
"type": "taxpayer",
"_links": {
"self": {"href": "/taxpayers/TP123456"},
"taxReturns": {
"href": "/tax-returns?taxpayerId=TP123456",
"type": "collection"
}
}
}4. Consistent Structure
All resources follow the same pattern:
{
"id": "...", // Required: Unique identifier
"type": "...", // Required: Resource type
// ... resource fields ...
"_links": { /* ... */ } // Required: Relationships
}5. Path-Only URLs
Use path-only URLs in _links for flexibility:
✅ Good:
"_links": {
"self": {"href": "/taxpayers/TP123456"}
}❌ Avoid:
"_links": {
"self": {"href": "http://api.example.com/taxpayers/TP123456"}
}Why: Path-only URLs work across different environments (dev, staging, prod) without modification.
Creating a New API
Step 1: Define Your Domain
Identify:
- What resources does this API manage?
- What are the key entities and their attributes?
- How do these resources relate to other domains?
Example: Payment API
- Resources: Payment, PaymentAllocation
- Relationships: To Taxpayer (who made payment), To TaxReturn (what payment is for)
Step 2: Create the OpenAPI Specification
Create a new file: specs/your-api/your-api.yaml
openapi: 3.0.3
info:
title: Your API
version: 1.0.0
description: |
Description of your API's purpose and capabilities.
servers:
- url: http://localhost:8084
description: Local mock server
paths:
/your-resources:
get:
summary: List resources
operationId: listResources
parameters:
- $ref: '../shared/shared-components.yaml#/components/parameters/IncludeParameter'
responses:
'200':
description: List of resources
content:
application/json:
schema:
type: object
required:
- items
- _links
properties:
items:
type: array
items:
$ref: '#/components/schemas/YourResource'
_links:
$ref: '../shared/shared-components.yaml#/components/schemas/Links'
components:
schemas:
YourResource:
type: object
required:
- id
- type
- _links
properties:
id:
type: string
description: Unique identifier
type:
type: string
enum: [your-resource]
_links:
allOf:
- $ref: '../shared/shared-components.yaml#/components/schemas/Links'
- type: object
properties:
relatedResource:
type: object
required: [href]
properties:
href:
type: string
format: uri-reference
type:
type: string
title:
type: stringStep 3: Validate Your Specification
npm run validateFix any validation errors before proceeding.
Step 4: Test with Mock Server
# Add to package.json scripts:
"mock:your-api": "prism mock specs/your-api/your-api.yaml -p 8084"
# Start the mock server
npm run mock:your-api
# Test it
curl http://localhost:8084/your-resourcesResource Structure
Required Fields
Every resource MUST have:
YourResource:
type: object
required:
- id # Unique identifier
- type # Resource type
- _links # Relationships
properties:
id:
type: string
pattern: '^[A-Z]{2}[0-9]+$' # Define your ID format
example: "YR123456"
type:
type: string
enum: [your-resource]
example: "your-resource"
_links:
$ref: '../shared/shared-components.yaml#/components/schemas/Links'Resource-Specific Fields
Add your domain-specific fields:
properties:
# ... required fields above ...
# Your domain fields
name:
type: string
description: Resource name
example: "Example Resource"
amount:
$ref: '../shared/shared-components.yaml#/components/schemas/Money'
date:
type: string
format: date
example: "2024-01-15"
status:
type: string
enum: [draft, active, closed]
example: "active"Examples
Always provide examples:
YourResource:
type: object
# ... properties ...
example:
id: "YR123456"
type: "your-resource"
name: "Example Resource"
amount:
amount: 1000.00
currency: "GBP"
_links:
self:
href: "/your-resources/YR123456"Defining Relationships
Single Resource Relationship
Link to one related resource:
_links:
allOf:
- $ref: '../shared/shared-components.yaml#/components/schemas/Links'
- type: object
properties:
taxpayer:
type: object
required: [href]
properties:
href:
type: string
format: uri-reference
description: URL to the taxpayer
example: "/taxpayers/TP123456"
type:
type: string
enum: [taxpayer]
title:
type: string
example: "Taxpayer who owns this resource"Collection Relationship
Link to multiple related resources:
_links:
allOf:
- $ref: '../shared/shared-components.yaml#/components/schemas/Links'
- type: object
properties:
taxReturns:
type: object
required: [href]
properties:
href:
type: string
format: uri-reference
description: URL to tax returns collection
example: "/tax-returns?taxpayerId=TP123456"
type:
type: string
enum: [collection]
title:
type: string
example: "Tax returns for this taxpayer"Cross-API Relationships
Relationships can point to other APIs:
# In Taxpayer API, link to Income Tax API
_links:
taxReturns:
href: "/income-tax/v1/tax-returns?taxpayerId=TP123456"
type: "collection"Important: Use path-only URLs. The gateway will handle routing to the correct API.
Bidirectional Relationships
Ensure relationships work both ways:
Taxpayer → Tax Returns:
# In Taxpayer resource
_links:
taxReturns:
href: "/tax-returns?taxpayerId=TP123456"
type: "collection"Tax Return → Taxpayer:
# In TaxReturn resource
_links:
taxpayer:
href: "/taxpayers/TP123456"
type: "taxpayer"Using Shared Components
Available Shared Components
Located in specs/shared/shared-components.yaml:
Schemas
Address: UK postal address with postcode validationMoney: Monetary amount in GBPDateRange: Start and end datesLinks: Hypermedia links structure
Parameters
IncludeParameter: Query parameter for including related resources
Responses
NotFound: 404 error responseBadRequest: 400 error responseBadGateway: 502 error response
Using Shared Schemas
Reference shared schemas with $ref:
YourResource:
properties:
address:
$ref: '../shared/shared-components.yaml#/components/schemas/Address'
amount:
$ref: '../shared/shared-components.yaml#/components/schemas/Money'
period:
$ref: '../shared/shared-components.yaml#/components/schemas/DateRange'
_links:
$ref: '../shared/shared-components.yaml#/components/schemas/Links'Using Shared Parameters
paths:
/your-resources:
get:
parameters:
- $ref: '../shared/shared-components.yaml#/components/parameters/IncludeParameter'Using Shared Responses
paths:
/your-resources/{id}:
get:
responses:
'200':
# ... success response ...
'404':
$ref: '../shared/shared-components.yaml#/components/responses/NotFound'Adding New Shared Components
If you create a component that multiple APIs will use:
- Add it to
specs/shared/shared-components.yaml - Document its purpose and usage
- Provide examples
- Update this guide
OpenAPI Best Practices
1. Use Descriptive Operation IDs
paths:
/taxpayers:
get:
operationId: listTaxpayers # Clear and unique2. Provide Comprehensive Descriptions
paths:
/taxpayers/{id}:
get:
summary: Get taxpayer details
description: |
Retrieve detailed information about a specific taxpayer.
Supports including related resources using the `include` parameter.
Available relationships: taxReturns, payments3. Define All Response Codes
responses:
'200':
description: Success
'400':
$ref: '../shared/shared-components.yaml#/components/responses/BadRequest'
'404':
$ref: '../shared/shared-components.yaml#/components/responses/NotFound'
'500':
description: Internal server error4. Use Enums for Fixed Values
status:
type: string
enum: [draft, submitted, assessed, closed]
description: |
Current status:
- draft: Being prepared
- submitted: Awaiting assessment
- assessed: Tax calculated
- closed: Finalized5. Validate with Patterns
nino:
type: string
pattern: '^[A-Z]{2}[0-9]{6}[A-Z]$'
description: National Insurance Number
example: "AB123456C"6. Provide Multiple Examples
examples:
minimal:
summary: Minimal required fields
value:
taxpayerId: "TP123456"
taxYear: "2023-24"
complete:
summary: Complete with all optional fields
value:
taxpayerId: "TP123456"
taxYear: "2023-24"
totalIncome:
amount: 50000.00
currency: "GBP"Testing Your API
Validation Tests
Ensure your OpenAPI spec is valid:
npm run validate
npm run lintMock Server Tests
Test your API design before implementation:
# Start mock server
npm run mock:your-api
# Test endpoints
curl http://localhost:8084/your-resources
curl http://localhost:8084/your-resources/YR123456Property-Based Tests
Write tests that verify correctness properties:
// tests/property/your-api.test.js
describe('Your API - Property Tests', () => {
test('All resources have required fields', () => {
const spec = loadOpenAPISpec('specs/your-api/your-api.yaml');
for (const [name, schema] of Object.entries(spec.components.schemas)) {
if (isResourceSchema(schema)) {
expect(schema.required).toContain('id');
expect(schema.required).toContain('type');
expect(schema.required).toContain('_links');
}
}
});
test('All relationship links use path-only URLs', () => {
const spec = loadOpenAPISpec('specs/your-api/your-api.yaml');
for (const example of extractExamples(spec)) {
if (example._links) {
for (const [rel, link] of Object.entries(example._links)) {
if (rel !== 'self' && link.href) {
expect(link.href).toMatch(/^\/[^/]/); // Starts with / but not //
expect(link.href).not.toMatch(/^https?:\/\//);
}
}
}
}
});
});Integration Tests
Test cross-API relationships:
// tests/integration/cross-api.test.js
describe('Cross-API Integration', () => {
test('Can traverse from taxpayer to tax returns', async () => {
// Get taxpayer
const taxpayer = await fetch('/taxpayers/TP123456').then(r => r.json());
// Extract tax returns link
const taxReturnsUrl = taxpayer._links.taxReturns.href;
expect(taxReturnsUrl).toBeDefined();
// Follow link
const taxReturns = await fetch(taxReturnsUrl).then(r => r.json());
expect(taxReturns.items).toBeInstanceOf(Array);
});
});Implementation Guidelines
Backend Implementation Checklist
When implementing the backend for your API:
- Implement all endpoints defined in the OpenAPI spec
- Return correct status codes (200, 201, 400, 404, etc.)
- Include
_linksin all responses with correct relationship URLs - Support the
includeparameter (or let the gateway handle it) - Validate request data against the schema
- Handle errors gracefully with proper error responses
- Add logging and monitoring
- Write unit tests for business logic
- Write integration tests for API endpoints
- Document any deviations from the spec
Include Parameter Implementation
You have two options for implementing the include parameter:
Option 1: Let the Gateway Handle It (Recommended)
Your API doesn’t need to know about include:
// Your API just returns the resource with _links
app.get('/taxpayers/:id', async (req, res) => {
const taxpayer = await db.getTaxpayer(req.params.id);
res.json({
id: taxpayer.id,
type: 'taxpayer',
nino: taxpayer.nino,
name: taxpayer.name,
_links: {
self: { href: `/taxpayers/${taxpayer.id}` },
taxReturns: {
href: `/tax-returns?taxpayerId=${taxpayer.id}`,
type: 'collection'
}
}
});
});The gateway will:
- Receive the request with
?include=taxReturns - Call your API to get the taxpayer
- Extract the
taxReturnslink - Call the Income Tax API
- Merge the responses
- Return the combined result
Option 2: Implement It Yourself
If you want to handle includes in your API:
app.get('/taxpayers/:id', async (req, res) => {
const taxpayer = await db.getTaxpayer(req.params.id);
const response = {
id: taxpayer.id,
type: 'taxpayer',
nino: taxpayer.nino,
name: taxpayer.name,
_links: {
self: { href: `/taxpayers/${taxpayer.id}` },
taxReturns: {
href: `/tax-returns?taxpayerId=${taxpayer.id}`,
type: 'collection'
}
}
};
// Handle include parameter
const includes = req.query.include?.split(',') || [];
if (includes.includes('taxReturns')) {
const taxReturns = await fetchTaxReturns(taxpayer.id);
response._includes = {
taxReturns: taxReturns.map(tr => tr.id)
};
response._included = {
taxReturns: taxReturns
};
}
res.json(response);
});Error Response Format
Always use the standard error format:
// 404 Not Found
res.status(404).json({
error: {
code: 'RESOURCE_NOT_FOUND',
message: 'Taxpayer TP123456 not found',
status: 404
}
});
// 400 Bad Request with validation details
res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid request data',
status: 400,
details: [
{
field: 'nino',
message: 'Must match pattern ^[A-Z]{2}[0-9]{6}[A-Z]$'
}
]
}
});Using Kamelets for Backend Orchestration
Kamelets are reusable route templates for calling backend services. They encapsulate common patterns like HTTP calls, XML-to-JSON transformation, and response handling.
Kamelet Location
Kamelets are stored in:
specs/<api-name>/domain/platform/kamelets/
Basic Kamelet Structure
apiVersion: camel.apache.org/v1
kind: Kamelet
metadata:
name: customer-getCustomer
spec:
definition:
title: Get Customer
properties:
customerId:
type: string
template:
from:
uri: kamelet:source
steps:
- removeHeaders:
pattern: "CamelHttp*"
- setHeader:
name: CamelHttpMethod
constant: GET
- toD:
uri: "http://customer-proxy:4010/customers/{{customerId}}"
- setProperty:
name: customerResponse
simple: "${body}"
- to: kamelet:sinkCritical: Unique RouteIds for Each Kamelet Invocation
When calling Kamelets from routes, each Kamelet invocation must have a unique routeId. Without this, Camel cannot properly track which route to return to after the Kamelet completes, causing execution to jump to wrong routes.
Syntax:
- to:
uri: "kamelet:<kamelet-name>/<unique-route-id>?param1=value1¶m2=value2"Example - CORRECT (unique routeIds per invocation):
# In get-submission-return-by-ack.yaml
- route:
id: getSubmissionReturnByAck
from:
uri: direct:getSubmissionReturnByAck
steps:
- to:
uri: "kamelet:tax-platform-getSubmission/ack-tp?acknowledgementReference=${exchangeProperty.ackRef}"
- to:
uri: "kamelet:excise-getRegistration/ack-reg?vpdApprovalNumber=${exchangeProperty.vpdApprovalNumber}"
- to:
uri: "kamelet:excise-getPeriod/ack-period?periodKey=${exchangeProperty.periodKey}"
- to:
uri: "kamelet:customer-getCustomer/ack-cust?customerId=${exchangeProperty.customerId}"Note how each Kamelet call uses a unique routeId suffix:
ack-tp- tax-platform in the ack routeack-reg- excise registration in the ack routeack-period- excise period in the ack routeack-cust- customer in the ack route
Example - INCORRECT (same routeId for multiple invocations):
# DON'T DO THIS - same routeId causes routing bugs!
- to:
uri: "kamelet:tax-platform-getSubmission/get-by-ack?..."
- to:
uri: "kamelet:excise-getRegistration/get-by-ack?..." # WRONG: same routeIdRouteId Naming Conventions
Use a consistent naming scheme for routeIds:
| Route | Kamelet | RouteId |
|---|---|---|
| get-submission-return-by-ack | tax-platform-getSubmission | ack-tp |
| get-submission-return-by-ack | excise-getRegistration | ack-reg |
| get-submission-return-by-ack | customer-getCustomer | ack-cust |
| get-submission-return-by-approval | excise-getRegistration | appr-reg |
| get-submission-return-by-approval | customer-getCustomer | appr-cust |
| post-submission-return | excise-validateAndCalculate | post-exc |
| post-submission-return | customer-getCustomer | post-cust |
The pattern is: <route-abbreviation>-<kamelet-abbreviation>
Kamelet Invocation Syntax
Use the to: URI syntax with routeId:
# Correct - to: with uri and routeId
- to:
uri: "kamelet:customer-getCustomer/ack-cust?customerId=${exchangeProperty.customerId}"
# Incorrect - YAML DSL kamelet: syntax doesn't support routeId
- kamelet:
name: customer-getCustomer/ack-cust # This won't work!Passing Parameters
Parameters are passed as query parameters in the URI:
- to:
uri: "kamelet:excise-getRegistration/ack-reg?vpdApprovalNumber=${exchangeProperty.vpdApprovalNumber}"The Kamelet receives these as template properties:
# In excise-getRegistration.kamelet.yaml
spec:
definition:
properties:
vpdApprovalNumber:
type: string
template:
from:
steps:
- toD:
uri: "http://excise-proxy:4010/registrations/{{vpdApprovalNumber}}"Storing Response Data
Kamelets should store response data in exchange properties for downstream consumption:
# In the Kamelet
- setProperty:
name: customerResponse
simple: "${body}"
- to: kamelet:sink
# In the calling route - access the stored response
- setBody:
groovy: |
def customer = exchange.getProperty('customerResponse')
// Use customer dataCommon Patterns
Pattern 1: Paginated Collections
paths:
/your-resources:
get:
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
'200':
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/YourResource'
_links:
type: object
properties:
self:
type: string
next:
type: string
prev:
type: stringPattern 2: Filtered Collections
paths:
/tax-returns:
get:
parameters:
- name: taxpayerId
in: query
schema:
type: string
- name: taxYear
in: query
schema:
type: string
- name: status
in: query
schema:
type: string
enum: [draft, submitted, assessed, closed]Pattern 3: Sub-Resources
paths:
/tax-returns/{id}/assessments:
get:
summary: Get assessments for a tax return
parameters:
- name: id
in: path
required: true
schema:
type: stringChecklist for New APIs
Before considering your API complete:
Design Phase
- Domain boundaries are clear
- Resources are well-defined
- Relationships are identified
- OpenAPI spec is created
- Shared components are used where appropriate
- Examples are provided for all schemas
Validation Phase
- OpenAPI spec validates successfully
- Spectral linting passes
- Mock server runs without errors
- All endpoints are testable via mock server
Testing Phase
- Property-based tests written and passing
- Integration tests cover cross-API relationships
- Error cases are tested
- Include parameter behavior is tested
Documentation Phase
- API purpose is documented
- All endpoints have descriptions
- Relationship semantics are clear
- Examples are comprehensive
- Generated docs are reviewed
Implementation Phase
- Backend implements all endpoints
- Responses match OpenAPI spec
- Error handling is consistent
- Logging and monitoring are in place
- Performance is acceptable
Next Steps
- Read the API Consumer Guide to understand the consumer perspective
- Review existing APIs in
specs/for examples - Check the design document for architecture details
- Explore testing strategies
Support
For questions or issues:
- Review the OpenAPI 3.0 specification
- Check the design document
- Consult existing API specs for patterns
- Ask the team for guidance