Build Your First API

A step-by-step tutorial: register a GET endpoint, search your storage, reply in JSON, validate request parameters, secure the endpoint with scopes, and test it with curl.

In this tutorial you build a real API endpoint: GET /api/v1/tasks, which returns the open tasks from the tasks storage as JSON. It picks up where the Quickstart left off — the quickstart shows the smallest possible endpoint in five minutes; this page goes deeper into URIs, storage queries, validation, and security. If you followed Build Your First CRUD App, you already have the tasks storage this endpoint reads from.

1. 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.

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

2. Define the Endpoint

The trigger block carries the endpoint definition. Set the URI field to v1/tasks — the block exposes URIs under /api/, so your endpoint becomes GET /api/v1/tasks. Prefixing with a version like v1 is a convention, not a requirement, but it lets you introduce v2 later without breaking existing consumers.

The seeded block answers GET requests. Other methods have their own trigger blocks — for example on_startup_register_uri_post for POST — and a single blueprint can hold several of them when the endpoints belong together.

URIs can be multiple segments deep and may include named parameters such as v1/tasks/:guid. You will read those parameters in step 5.

Keep the URI lowercase and plural — v1/tasks, not v1/getTasks. The URI is the public contract of your endpoint.

3. Create the Handler Function

The trigger block does not contain logic itself — it hands each request to a function. Drag from the block's function 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 page flow and an API endpoint need the same query logic.

4. Read Data 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, desc) 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.

The list_tasks flow: on api get triggering a function that searches the tasks storage and replies in JSON

5. Validate the Request

Real endpoints check their input before touching storage. Say you support an optional status parameter in the URI, declared as v1/tasks/:status. Read it with httpconnection_get_params — connect httpconnection_current_request to its connection in-pin, and it outputs a params object containing status.

To make a parameter required, check it before running the search:

  1. Add a condition_not_empty_value block and connect the status param to it.
  2. Add a branch block, connect the function's flow and the condition. It splits execution into true and false paths.
  3. On the false path, 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. On the true path, run the search from step 4 — now using the param value in the term query instead of a fixed value.
Validation branch: get params, is-not-empty condition, and a 400 reply on the false path

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.

6. Scopes and Security

New endpoints use the *public scope by default — anyone on the internet can call them. Click the lock icon on the trigger block to open the Scopes Management Modal and change that:

  • *loggedin — restrict the endpoint to users 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 scopes — create your own (for example tasks-read) and assign them 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 same modal 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, allowing one request per interval). Both are explained in detail under Remote Access Control.

Built-in APIs Have Their Own Limits The cluster's public system APIs carry a fixed default rate limit of 100 requests per second that cannot be changed cluster-wide. Custom endpoints like the one you just built have no such fixed limit — add a rate limit or throttle per endpoint when you need one.

7. Activate and Test

APIs are inactive by default. Save the blueprint (Cmd/Ctrl + S), then click Activate in the top bar. To test the saved — not yet deployed — version, call the endpoint in development view by appending ?development:

Terminal
curl "https://<your-cluster>/api/v1/tasks?development"
Response
[
  {
    "_meta": {
      "created": 1752768000,
      "guid": "b34686f50e1b04964250f13391274d85308c129a793c6ec97bc41870fc67081e",
      "updated": 1752768000
    },
    "created": 1752768000,
    "owner_guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
    "owner_name": "joe doe",
    "status": "open",
    "title": "write the release notes"
  }
]

A 401 or 403 means the scope check failed — verify the lock icon shows the intended scope and that your token is passed correctly. Consumers only see the endpoint after you deploy the blueprint to production.

8. 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. Where to go from here:

Cluster APIs Your cluster ships 40+ built-in REST APIs 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: Hello World page plus a minimal JSON endpoint.