← Back to Documentation

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:

OpenAPI Contract File

Location

MyApp/
├── openapi.yaml          # Required for HTTP server
├── main.aro
└── users.aro

Accepted filenames (in order of precedence):

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

VariableDescription
pathParametersDictionary of path parameters
pathParameters.{name}Individual parameter
queryParametersDictionary of query parameters
request.bodyParsed request body
request.headersRequest 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:

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 PathMatchesPath Parameters
/users/usersnone
/users/{id}/users/123{id: "123"}
/users/{id}/orders/{orderId}/users/123/orders/456{id: "123", orderId: "456"}

Method Mapping

HTTP MethodOpenAPI FieldExample operationId
GETgetlistUsers, getUser
POSTpostcreateUser
PUTputupdateUser
PATCHpatchpatchUser
DELETEdeletedeleteUser

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