Contract-First Development
ARO uses OpenAPI contract-first API development, where an openapi.yaml
file becomes the source of truth for HTTP routing and request/response validation.
No Contract = No Server
Without an openapi.yaml file, the HTTP server does NOT start and no port is opened.
This enforces the contract-first approach.
Why Contract-First?
Contract-first API development offers significant advantages:
- Single Source of Truth: The OpenAPI specification defines the API contract
- Documentation-Driven: API is designed before implementation
- Type Safety: Request/response schemas are validated
- Tooling Integration: Generate clients, documentation, tests from spec
- Team Alignment: Frontend and backend teams agree on contract upfront
OpenAPI Contract File
Location
MyApp/
├── openapi.yaml # Required for HTTP server
├── main.aro
└── users.aro
Accepted filenames (in order of precedence):
openapi.yamlopenapi.ymlopenapi.json
Required Structure
openapi: 3.0.3
info:
title: My API
version: 1.0.0
paths:
/users:
get:
operationId: listUsers # Maps to ARO feature set name
responses:
'200':
description: Success
Key Requirement: Every operation MUST have an operationId.
This maps directly to ARO feature set names.
Feature Set Mapping
operationId to Feature Set
Feature sets are named after OpenAPI operationId values:
# openapi.yaml
paths:
/users:
get:
operationId: listUsers
post:
operationId: createUser
/users/{id}:
get:
operationId: getUser
(* users.aro - Feature sets match operationIds *)
(listUsers: User API) {
<Retrieve> the <users> from the <user-repository>.
<Return> an <OK: status> with <users>.
}
(createUser: User API) {
<Extract> the <data> from the <request: body>.
<Create> the <user> with <data>.
<Return> a <Created: status> with <user>.
}
(getUser: User API) {
<Extract> the <id> from the <pathParameters: id>.
<Retrieve> the <user> from the <user-repository> where id = <id>.
<Return> an <OK: status> with <user>.
}
Validation at Startup
Missing handlers cause startup failure:
Error: Missing ARO feature set handlers for the following operations:
- GET /users requires feature set named 'listUsers'
- POST /users requires feature set named 'createUser'
Create feature sets with names matching the operationIds in your OpenAPI contract.
Path Parameters
Extraction
Path parameters defined in OpenAPI are automatically extracted:
paths:
/users/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
(getUser: User API) {
<Extract> the <user-id> from the <pathParameters: id>.
(* user-id contains the path parameter value *)
}
Available Context
| Variable | Description |
|---|---|
pathParameters | Dictionary of path parameters |
pathParameters.{name} | Individual parameter |
queryParameters | Dictionary of query parameters |
request.body | Parsed request body |
request.headers | Request headers |
Schema Binding
Request Body Typing
Request bodies are parsed according to OpenAPI schemas:
components:
schemas:
CreateUserRequest:
type: object
properties:
name:
type: string
minLength: 1
email:
type: string
format: email
required:
- name
- email
(createUser: User API) {
<Extract> the <data> from the <request: body>.
(* data is parsed and validated against CreateUserRequest schema *)
(* data.name and data.email are available *)
}
Validation
Schema validation includes:
- Required properties
- Type checking (string, number, boolean, array, object)
- Format validation (email, date-time, uuid)
- Constraints (minLength, maxLength, minimum, maximum, pattern)
HTTP Server Behavior
With Contract
$ aro run ./MyApp
Loading openapi.yaml...
Validating contract against feature sets...
✓ listUsers -> GET /users
✓ createUser -> POST /users
✓ getUser -> GET /users/{id}
HTTP Server started on port 8080
Without Contract
$ aro run ./MyApp
No openapi.yaml found - HTTP server disabled
Application running (no HTTP routes available)
Route Matching
Path Patterns
| OpenAPI Path | Matches | Path Parameters |
|---|---|---|
/users | /users | none |
/users/{id} | /users/123 | {id: "123"} |
/users/{id}/orders/{orderId} | /users/123/orders/456 | {id: "123", orderId: "456"} |
Method Mapping
| HTTP Method | OpenAPI Field | Example operationId |
|---|---|---|
| GET | get | listUsers, getUser |
| POST | post | createUser |
| PUT | put | updateUser |
| PATCH | patch | patchUser |
| DELETE | delete | deleteUser |
Complete Example
openapi.yaml
openapi: 3.0.3
info:
title: User Service API
version: 1.0.0
paths:
/users:
get:
operationId: listUsers
summary: List all users
responses:
'200':
description: List of users
post:
operationId: createUser
summary: Create a user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
/users/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
operationId: getUser
summary: Get user by ID
responses:
'200':
description: User found
'404':
description: User not found
components:
schemas:
CreateUserRequest:
type: object
properties:
name:
type: string
email:
type: string
required:
- name
- email
users.aro
(* Feature sets named after operationIds *)
(listUsers: User API) {
<Retrieve> the <users> from the <user-repository>.
<Return> an <OK: status> with <users>.
}
(getUser: User API) {
<Extract> the <id> from the <pathParameters: id>.
<Retrieve> the <user> from the <user-repository> where id = <id>.
if <user> is empty then {
<Return> a <NotFound: status> for the <request>.
}
<Return> an <OK: status> with <user>.
}
(createUser: User API) {
<Extract> the <data> from the <request: body>.
<Create> the <user> with <data>.
<Store> the <user> into the <user-repository>.
<Emit> a <UserCreated: event> with <user>.
<Return> a <Created: status> with <user>.
}
Next Steps
HTTP Services - Building REST APIs with ARO
Feature Sets - Organizing your code