Build Your First API

Build GET /api/v1/tasks: an endpoint that takes a status, searches the tasks storage and replies in JSON. On the way you validate the request, secure it with a scope and test it with curl.

This page picks up where the Quickstart left off. The quickstart shows the smallest possible endpoint; this page goes deeper into URLs, storage queries, validation and security. If you followed Build Your First CRUD App, you already have the tasks storage this endpoint reads from.

Create the API blueprint

An API endpoint lives in its own blueprint. Open the blueprints overview in RUAL Studio, create a new blueprint, and choose the type Build an API endpoint. This seeds the canvas with an on_startup_register_uri_get block: the trigger that starts your flow on every incoming request.

Name the blueprint after what it serves, for example Tasks API. One blueprint per endpoint (or per small group of related endpoints) keeps flows readable, similar to assigning a single class to a file in traditional coding.

apiqs create blueprint
Creating a new blueprint of type Build an API endpoint, named Tasks API
Creating a new blueprint of type Build an API endpoint, named Tasks API

Define the endpoint on the trigger block

The trigger block carries the endpoint definition. Click its no url given button and type tasks. The block exposes URLs under /api/ with a version, v1 by default, so your endpoint becomes GET /api/v1/tasks. Its version pin lets you introduce v2 later without breaking existing consumers.

  • Other methods have their own trigger blocks, for example on_startup_register_uri_post for POST. A single blueprint can hold several of them when the endpoints belong together.
  • URLs can be multiple segments deep and may include named parameters such as tasks/:guid. Read those with httpconnection_get_params.
  • Keep the URL lowercase and plural: tasks, not getTasks. It is the public contract of your endpoint.

Hand each request to a function

The trigger block does not contain logic itself. It hands each request to a function. Drag from the block's On Request out-pin into an empty area of the canvas and create a trigger_custom_function, naming it list_tasks. From then on, the function's flow out-pin runs on every request to GET /api/v1/tasks.

Functions are private to their blueprint by default and can be reused from other flows with function_custom_execute: handy when a web page and an API endpoint need the same query logic.

Search the storage and reply in JSON

Inside the function, search the tasks storage and send the results back with httpconnection_set_json. Wire the blocks like this:

Block Purpose Connects to
storage (tasks) Selects the storage to search function_search (storage pin)
query_bool_term_fields (status = open) Match only open tasks query_bool_filter
query_bool_filter Wrap the term query in an efficient filter context query_and
query_sort_field (_meta.created) Newest tasks first query_and
query_and Combine filter and sort into one query function_search (query pin)
number_default (50) Limit the result set function_search (limit pin)
function_search Runs the search; outputs an array of documents httpconnection_set_json (data pin)
httpconnection_current_request Reference to the incoming request httpconnection_set_json (connection pin)
number_default (200) The HTTP status code httpconnection_set_json (code pin)

Connect the function's flow out-pin to the flow in-pin of function_search, and from there to httpconnection_set_json. The flow pins control execution order, the data pins carry the results. Query composition is covered in depth under Building Search Queries.

apiqs list tasks flow
The shape of the flow: On API GET, a function, a search on the tasks storage and a reply in JSON. The URI value in this shot differs; use tasks.
The shape of the flow: On API GET, a function, a search on the tasks storage and a reply in JSON. The URI value in this shot differs; use tasks.

Validate the request before touching storage

Real endpoints check their input first. Say the caller has to name a status: GET /api/v1/tasks?status=open. Read it with httpconnection_get_query: connect httpconnection_current_request to its connection in-pin, and it outputs a query object containing status.

  1. Check the value

    Add a condition_not_empty_value block and connect the status value to it.

  2. Branch on it

    Add a branch block and connect the function's flow and the condition. It splits execution into true and false paths.

  3. Refuse on false

    Reply with a second httpconnection_set_json: a number_default of 400 on the code pin and an object_new_fields with an error field such as missing required parameter: status on the data pin.

  4. Search on true

    Run the search from the previous section, now using the value in the term query instead of a fixed one.

apiqs validation flow
A validation branch: read a value, is-not-empty condition, and a 400 reply on the false path. This shot reads a URL parameter with get params; a query string works the same with get query. Its URI value differs too.
A validation branch: read a value, is-not-empty condition, and a 400 reply on the false path. This shot reads a URL parameter with get params; a query string works the same with get query. Its URI value differs too.

Blocks like function_search also expose success and error out-pins. Use them to return a 500 with a useful message instead of failing silently. When a flow misbehaves, Debugging shows how to trace execution inside the blueprint.

The whole endpoint in the code view, including the scope from the next section. You can paste it instead of wiring the blocks:

Tasks API
blueprint "Tasks API"

route GET "tasks" -> list-tasks with {scopes: ["*loggedin"]}

fn list-tasks(connection, params, query, body) {
  guard condition_not_empty_value(query.status) else {
    reply 400 {error: "missing required parameter: status"}
  }
  tasks = search@tasks(
    query: query_and(query_bool_filter(term{status: query.status}), query_sort_field[_meta.created]()),
    limit: 50,
  )
  guard tasks.success else {
    reply 500 {error: tasks.error}
  }
  reply 200 array: tasks.array
}

Name a scope, or the route stays locked

A route without any scope answers 403 with ENDPOINT_NOT_PUBLIC to everyone. Click the lock icon on the trigger block to open the Scopes Management Modal and choose:

Scope Who may call
*publicAnyone on the internet.
*loggedinUsers with a valid access_token. Tokens are issued by the login APIs, are valid for 14 days, and are extended automatically while actively used.
Custom scopesCreate your own (for example tasks-read) and assign it to only the users or groups that may call this endpoint.

Callers can pass a token five ways: an Authorization: Bearer header, an x-authtoken or x-token header, an access_token cookie, or ?access_token= in the query string.

The block also holds two per-endpoint protection switches: a rate limit (key type, max requests, timeframe in seconds: extra requests are blocked) and a throttle (key type plus a timeout: requests with the same key run one at a time). Both are explained in detail under Remote Access Control.

Your endpoint already has a baseline limit An endpoint without a rate limit of its own falls under the cluster baseline: by default 1200 requests a minute per client address for anonymous callers, and 600 a minute per session for signed-in ones. See Rate Limiting.

Activate it and test with curl

APIs are inactive by default. Save the blueprint (Cmd/Ctrl + S), then click the switch that reads Inactive in the top bar. Saving is what makes a change live, so the endpoint now answers everyone its scope allows:

Terminal
curl -H "Authorization: Bearer YOUR_TOKEN" "https://<your-cluster>/api/v1/tasks?status=open"
Response
[
  {
    "_meta": {
      "cms": 1752768000362,
      "created": 1752768000,
      "entity": 1,
      "expiry": -1,
      "guid": "b34686f50e1b04964250f13391274d85308c129a793c6ec97bc41870fc67081e",
      "removed": 0,
      "ums": 1752768000803,
      "update_hash": "60e27209fa93063b5605e41605c2722ed428cae0",
      "updated": 1752768000
    },
    "owner_guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
    "owner_name": "joe doe",
    "status": "open",
    "title": "write the release notes"
  }
]

A 401 means the token is missing or invalid; a 403 means the scope check failed. Verify the lock icon shows the intended scope and that your token is passed correctly. To try a change before saving it, simulate the function: a simulation runs your unsaved edits and writes nothing.

Next steps

You registered a versioned GET endpoint, wired a storage query into a JSON reply, validated input with a branch, and secured the endpoint.

API Guide Authentication, request conventions, error shapes and rate limits for every RUAL API. Cluster APIs Your cluster ships more than 240 built-in REST operations for managing users, storage and blueprints. Remote Access Control Scopes, the Scopes Management Modal, rate limiting and throttling in full detail. Quickstart The 15-minute version: a Hello World page plus a minimal JSON endpoint.

Frequently asked

How do I build a REST API endpoint in RUAL?

Register the route with an on_startup_register_uri_get block in its own blueprint, query storage in the flow, and reply with a JSON body. Then save and press Activate. Saving publishes it, and there is no separate deploy step on a current core.

How do I secure a custom RUAL API endpoint?

Assign a scope to the endpoint in the Scopes Management Modal. Callers then need a token carrying that scope. You can add a rate limit or a throttle per endpoint in the same modal.

Do custom RUAL endpoints have a rate limit by default?

Yes. An endpoint without a limit of its own falls under the cluster baseline: by default 1200 requests a minute per client address when anonymous, and 600 a minute per session when signed in. Add a rate limit or throttle per endpoint when you need something tighter.