← Back to Documentation

HTTP Services

ARO provides built-in HTTP server and client capabilities. This chapter covers how to build web APIs and make HTTP requests.

HTTP Server

Starting the Server

Start an HTTP server in Application-Start:

(Application-Start: Web API) {
    (* Routes and port come from openapi.yaml — no contract, no server. *)
    Start the <http-server> with { }.
    Keepalive the <application> for the <events>.
    Return an <OK: status> for the <startup>.
}

Route Handlers (Contract-First)

Define routes in openapi.yaml and implement feature sets named after operationIds:

(listUsers: User API) {
    Retrieve the <users> from the <user-repository>.
    Return an <OK: status> with <users>.
}

(createUser: User API) {
    Extract the <user-data> from the <request: body>.
    Create the <user> with <user-data>.
    Store the <user> into the <user-repository>.
    Return a <Created: status> with <user>.
}

(updateUser: User API) {
    Extract the <user-id> from the <pathParameters: id>.
    Extract the <updates> from the <request: body>.
    Retrieve the <user> from the <user-repository> where id = <user-id>.
    Transform the <updated-user> from the <user> with <updates>.
    Store the <updated-user> into the <user-repository>.
    Return an <OK: status> with <updated-user>.
}

(deleteUser: User API) {
    Extract the <user-id> from the <pathParameters: id>.
    Delete the <user> from the <user-repository> where id = <user-id>.
    Return a <NoContent: status> for the <deletion>.
}

Request Data

Access request data using qualified variables:

(createUser: User API) {
    (* Path parameters *)
    Extract the <id> from the <pathParameters: id>.

    (* Query string *)
    Extract the <page> from the <queryParameters: page>.
    Extract the <limit> from the <queryParameters: limit>.

    (* Request body *)
    Extract the <data> from the <request: body>.

    (* Headers: pull the header set, then the header off it — a
       qualifier is a single name, so it cannot hold "headers X". *)
    Extract the <headers> from the <request: headers>.
    Extract the <auth-token> from the <headers: Authorization>.
    Extract the <content-type> from the <headers: Content-Type>.

    Return a <Created: status> with <data>.
}

Response Status Codes

Return appropriate status codes:

(* 2xx Success *)
Return an <OK: status> with <data>.           (* 200 *)
Return a <Created: status> with <resource>.   (* 201 *)
Return an <Accepted: status> for <async>.     (* 202 *)
Return a <NoContent: status> for <deletion>.  (* 204 *)

(* 4xx Client Errors *)
Return a <BadRequest: status> with <errors>.       (* 400 *)
Return an <Unauthorized: status> for <auth>.       (* 401 *)
Return a <Forbidden: status> for <access>.         (* 403 *)
Return a <NotFound: status> for <missing>.         (* 404 *)

(* 5xx Server Errors *)
Return an <InternalError: status> for <error>.     (* 500 *)

RESTful Patterns

Collection Resource

(* List all *)
(listProducts: Product API) {
    Extract the <limit> from the <queryParameters: limit>.

    Retrieve the <products> from the <product-repository>.
    Compute the <page-of-products: take> from <products> with <limit>.

    Return an <OK: status> with <page-of-products>.
}

(* Create new *)
(createProduct: Product API) {
    Extract the <product-data> from the <request: body>.
    Validate the <valid> for the <product-data>.
    Create the <product> with <product-data>.
    Store the <stored-product> into the <product-repository> with <product>.
    Return a <Created: status> with <stored-product>.
}

Single Resource

(* Get one *)
(getProduct: Product API) {
    Extract the <product-id> from the <pathParameters: id>.
    Retrieve the <product> from the <product-repository> where id = <product-id>.

    (* ARO has no `if`. A guard rides on the statement it protects. *)
    Return a <NotFound: status> for the <product-id> when <product> is empty.
    Return an <OK: status> with <product>.
}

(* Delete one *)
(deleteProduct: Product API) {
    Extract the <product-id> from the <pathParameters: id>.
    Delete the <removed> from the <product-repository> where id = <product-id>.
    Return a <NoContent: status> for the <removed>.
}

Nested Resources

(* User's orders *)
(listUserOrders: Order API) {
    Extract the <user-id> from the <pathParameters: userId>.
    Retrieve the <orders> from the <order-repository> where userId = <user-id>.
    Return an <OK: status> with <orders>.
}

(* Specific order for user — `and` joins where clauses *)
(getUserOrder: Order API) {
    Extract the <user-id> from the <pathParameters: userId>.
    Extract the <order-id> from the <pathParameters: orderId>.
    Retrieve the <order> from the <order-repository>
        where id = <order-id> and userId = <user-id>.

    Return a <NotFound: status> for the <order-id> when <order> is empty.
    Return an <OK: status> with <order>.
}

HTTP Client

Making Requests

Make outgoing HTTP requests:

(* Simple GET *)
Request the <data> from "https://api.example.com/resource".

(* GET with response handling *)
Request the <response> from "https://api.example.com/users".
Extract the <users> from the <response: body>.

Request Methods

(* GET *)
Request the <data> from the <users-url>.

(* POST — the `to` preposition is what makes it a POST *)
Request the <created> to the <users-url> with <user-data>.

(* PUT / DELETE — `via <METHOD>` names the verb explicitly *)
Request the <updated> via PUT the <user-url> with <updates>.
Request the <removed> via DELETE the <user-url>.

Response Handling

(Fetch External Data: External Service) {
    Request the <response> from "https://api.example.com/data".

    Extract the <status-code> from the <response: statusCode>.
    Extract the <body> from the <response: body>.

    Log "API error: ${status-code}" to the <console> when <status-code> != 200.
    Return a <BadRequest: status> for the <status-code> when <status-code> != 200.

    Return an <OK: status> with <body>.
}

Unified URL I/O

ARO provides unified I/O for both local files and remote URLs. The same Read and Write actions work with both file paths and HTTP URLs.

Reading from URLs (GET)

Use the url: system object to fetch data via HTTP GET:

(* Simple GET request *)
Read the <data> from the <url: "https://api.example.com/users">.

(* Response is auto-parsed based on Content-Type *)
Extract the <username> from the <data: name>.
Log "User: ${username}" to the <console>.

Writing to URLs (POST)

Use Write with a URL to send data via HTTP POST:

(* Create payload *)
Create the <payload> with {
    title: "Hello",
    body: "World",
    userId: 1
}.

(* POST to API *)
Write the <payload> to the <url: "https://api.example.com/posts">.

Custom Headers and Options

Use the with clause for headers, timeout, and other options:

(* GET with authentication *)
Read the <data> from the <url: "https://api.example.com/protected"> with {
    headers: {
        Authorization: "Bearer ${token}",
        Accept: "application/json"
    },
    timeout: 30
}.

(* POST with custom headers *)
Write the <payload> to the <url: "https://api.example.com/data"> with {
    headers: {
        Authorization: "Bearer ${token}",
        X-Request-ID: "${request-id}"
    }
}.

Data Pipeline Pattern

Seamlessly sync data between remote APIs and local files:

(Sync Data: Data Pipeline) {
    (* Fetch from remote API *)
    Read the <remotedata> from the <url: "https://api.example.com/export">.

    (* Cache locally *)
    Write the <remotedata> to the <file: "./data/snapshot.json">.

    (* Read local report *)
    Read the <report> from the <file: "./reports/daily.json">.

    (* Upload to remote *)
    Write the <report> to the <url: "https://api.example.com/upload">.

    Return an <OK: status> for the <sync>.
}

Authentication

API Key

(getSecureData: Secure API) {
    Extract the <headers> from the <request: headers>.
    Extract the <api-key> from the <headers: X-API-Key>.

    when <api-key> is empty {
        Return an <Unauthorized: status> for the <api-key>.
    }

    Validate the <key-valid> for the <api-key>.
    Return an <Unauthorized: status> for the <api-key> when not <key-valid>.

    Retrieve the <data> from the <secrets-repository>.
    Return an <OK: status> with <data>.
}

Bearer Token

(getProtectedResource: Protected API) {
    Extract the <headers> from the <request: headers>.
    Extract the <auth-header> from the <headers: Authorization>.

    when <auth-header> is empty {
        Return an <Unauthorized: status> for the <auth-header>.
    }

    (* "Bearer <token>" — split on the space, take the last field *)
    Split the <parts> from the <auth-header> by " ".
    Extract the <token> from the <parts: last>.

    Validate the <token-valid> for the <token>.
    Return an <Unauthorized: status> for the <token> when not <token-valid>.

    Retrieve the <data> from the <protected-repository>.
    Return an <OK: status> with <data>.
}

Best Practices

Validate Input

(createUser: User API) {
    Extract the <user-data> from the <request: body>.

    (* Always validate input. Validate binds a fresh boolean result. *)
    Validate the <valid> for the <user-data>.
    Return a <BadRequest: status> with <user-data> when not <valid>.

    Create the <user> with <user-data>.
    Return a <Created: status> with <user>.
}

Use Appropriate Status Codes

(* Be specific with status codes *)
Return a <Created: status> with <new-resource>.    (* Not just OK *)
Return a <NoContent: status> for the <deletion>.   (* Not OK with empty body *)
Return a <NotFound: status> for the <missing>.    (* Not BadRequest *)

Next Steps

Contract-First Development - OpenAPI integration
Sockets - TCP communication