packages/docs/concepts/api.md
Lowdefy APIs allow you to create custom server-side API endpoints within your Lowdefy application. These endpoints can execute complex server-side logic, orchestrate multiple database or external API calls, perform data transformations, and implement business workflows that require server-side processing.
APIs are particularly useful when you need to:
Lowdefy APIs are defined at the root level of your configuration, similar to connections and pages. Each API endpoint consists of a routine i.e. a sequence of steps and control structures that define the execution flow. When an API endpoint is called from the client using the CallAPI action, the routine executes on the server with access to connections, secrets, and server-side operators.
The API execution flow:
CallAPI action with an optional payload.lowdefy.yaml.API endpoints are defined in the api array at the root of your Lowdefy configuration. Each endpoint should have an id that is unique among all API endpoints in the app. The endpointId is used by the CallAPI action to specify which endpoint to execute.
The schema for a Lowdefy API is:
id: string: Required - A unique identifier for the API endpoint.type: string: Required - Either Api (callable from client pages and other endpoints) or InternalApi (callable only from other endpoints, not from client pages).routine: array/object: Required - The routine to execute. Operators are evaluated.lowdefy: '{{ version }}'
connections:
- id: users
type: MongoDBCollection
properties:
collection: users
databaseUri:
_secret: MONGODB_URI
api:
- id: get_user_data
type: Api
routine:
- id: fetch_user
type: MongoDBFindOne
connectionId: users
properties:
query:
user_id:
_payload: user_id
- :return:
_step: fetch_user
pages:
# ... your pages
Lowdefy APIs use the same authorization configuration as pages. By default, API endpoints are public (accessible without authentication), just like pages. Authorization is configured in the auth.api section of the lowdefy.yaml file.
Unlike requests which are defined on a specific page and inherit the authorization of the page they are defined on, API endpoints can be called from any page in the app. Thus they do not inherit the authorization of the page they are called from, and authorization should be configured separately.
lowdefy: '{{ version }}'
auth:
# Configure which APIs require authentication
api:
protected: true
public:
- health_check
roles:
admin:
- admin_api
- manage_users
manager:
- manager_reports
- team_data
api:
# Your API definitions
# ...
Routines define the execution logic of your API endpoint. Routines are defined as a nested combination of routines (subroutines). A routine can be one of:
Requests and connections can be used in API endpoints. Requests use the same connections defined in the connections section of the lowdefy.yaml file.
The payload property should not be defined for requests used in an API endpoint, the payload is specified by the CallAPI action.
A single request definition is a valid routine definition.
id: log_message
type: Api
routine:
id: insert_log_message
type: MongoDBInsertOne
connectionId: logs
properties:
message:
_payload: message
doc:
timestamp:
_date: now
Subroutines in an array execute one after another:
id: my_endpoint
type: Api
routine:
- id: step_1
type: MongoDBFindOne
connectionId: users
properties:
query:
_id:
_payload: user_id
- id: step_2
type: MongoDBInsertOne
connectionId: audit-log
properties:
doc:
user_id:
_step: step_1._id
action: 'user_viewed'
timestamp:
_date: now
- :return:
user:
_step: step_1
logged: true
Arrays can also be nested. This is useful when referencing shared subroutines that might be an array
# my_endpoint.yaml
id: my_endpoint
type: Api
routine:
- _ref: shared_routine.yaml
- :return:
user:
_step: step_1
logged: true
# shared_routine.yaml
- id: step_1
type: MongoDBFindOne
connectionId: users
properties:
query:
_id:
_payload: user_id
- id: step_2
type: MongoDBInsertOne
connectionId: audit-log
properties:
doc:
user_id:
_step: step_1._id
action: 'user_viewed'
timestamp:
_date: now
Control structures allow you to implement complex logic flows within your API routines. The following controls can be used in API routines:
:for - Iterate over an array sequentially.:if - Execute different routines based on a condition.:log - Output messages to the server console.:parallel - Execute multiple routines simultaneously.:parallel_for - Iterate over an array with concurrent processing.:reject - Return a user-facing error response.:return - Return a successful response with data.:set_state - Set values in server-side state.:switch - Handle multiple conditions with different outcomes.:throw - Throw a system error that can be caught.:try - Handle errors with catch and finally blocks.API endpoints can call other endpoints server-side using CallApi steps. This enables you to compose complex workflows from smaller, reusable endpoints without HTTP overhead.
A CallApi step has:
id: string: Required - A unique step id within the routine.type: CallApi: Required - Identifies this as an endpoint call step.properties.endpointId: string: Required - The id of the target endpoint. Operators are evaluated.properties.payload: object: Optional payload to pass to the target endpoint. Operators are evaluated.The called endpoint runs in an isolated context — it has its own _step results and _payload. Its internal step results do not appear in the calling endpoint's _step namespace. Only the value returned by the called endpoint's :return is stored as the step result.
api:
- id: process_order
type: Api
routine:
- id: validate
type: MongoDBFindOne
connectionId: orders
properties:
query:
_id:
_payload: order_id
- id: send_notification
type: CallApi
properties:
endpointId: send_email
payload:
to:
_step: validate.customer_email
subject: 'Order received'
- :return:
order:
_step: validate
email_sent:
_step: send_notification # Contains the :return value from send_email
- id: send_email
type: Api
routine:
- id: send
type: SendGridMail
connectionId: email
properties:
to:
_payload: to
subject:
_payload: subject
- :return:
success: true
Endpoint calls can be nested up to 10 levels deep. Exceeding this limit throws an error — this prevents accidental infinite recursion.
Endpoints with type: InternalApi are only callable from other endpoints via CallApi steps. They cannot be called from client pages using the CallAPI action, and HTTP requests to them return a "does not exist" error.
Use InternalApi for endpoints that contain sensitive server-side logic that should never be triggered directly from the client — for example, sending emails, processing payments, or auditing events.
api:
- id: charge_payment
type: InternalApi
routine:
- id: charge
type: AxiosHttp
connectionId: payment-gateway
properties:
data:
amount:
_payload: amount
token:
_payload: payment_token
- :return:
_step: charge
- id: checkout
type: Api # Callable from the client
routine:
- id: process_payment
type: CallApi
properties:
endpointId: charge_payment # Can call InternalApi endpoints
payload:
amount:
_payload: total
payment_token:
_payload: token
- :return:
payment:
_step: process_payment
If a client-side CallAPI action targets an InternalApi endpoint, the build produces a warning in dev mode and an error in production builds.
The following operators are specific to API endpoints:
_item - Access current item in :for and :parallel_for loops._state - Access server-side state set with :set_state._step - Access results from requests in previous steps.API endpoints also have access to all operators available on the server, including:
_payload - Access the payload sent from the client._secret - Access secrets from environment variables or secrets configuration._user - Access authenticated user information.Use the _step operator to access results from previously executed steps:
- id: get_user
type: MongoDBFindOne
connectionId: users
properties:
query:
email:
_payload: email
- id: get_orders
type: MongoDBAggregation
connectionId: orders
properties:
pipeline:
- $match:
user_id:
_step: get_user._id # Access the _id from the get_user request
Use the CallAPI action to call your API endpoints from pages. Read more about the CallAPI action here.
blocks:
- id: user_id_input
type: TextInput
properties:
title: User ID
- id: fetch_user_btn
type: Button
properties:
title: Fetch User Data
events:
onClick:
- id: call_user_api
type: CallAPI
params:
endpoint_id: get_user_data
payload:
user_id:
_state: user_id_input
- id: set_user_data
type: SetState
params:
user_data:
_actions: call_user_api.response.user
- id: user_display
type: Descriptions
properties:
items:
_state: user_data
events:
onClick:
try:
- id: call_process_data_api
type: CallAPI
params:
endpoint_id: process_data
payload:
data:
_state: form_data
- id: show_success
type: DisplayMessage
params:
content: 'Data processed successfully!'
status: success
- id: update_state
type: SetState
params:
result:
_actions: call_process_data_api.response
catch:
- id: update_state
type: SetState
params:
error_message:
_actions: call_process_data_api.error.message
The _api operator returns the response value of an API call. If the API has not yet been called, or is still executing, the returned value is null.
blocks:
- id: loading_text
type: Html
visible:
_api: fetch_data.loading
properties:
html: '<div class="secondary">Loading...</div>'
- id: error_alert
type: Alert
visible:
_not:
_api: fetch_data.success
properties:
type: error
message:
_api: fetch_data.error.message
- id: data_display
type: Descriptions
properties:
items:
_api: fetch_data.response
Here's a comprehensive example showing various features:
lowdefy: '{{ version }}'
auth:
api:
protected: true
roles:
user:
- process_order
api:
- id: process_order
type: Api
routine:
# Validate order
- id: validate_order
type: MongoDBFindOne
connectionId: orders
properties:
query:
_id:
_payload: order_id
status: 'pending'
user_id:
_user: id # Ensure user owns this order
- :if:
_not:
_step: validate_order
:then:
:reject: 'Order not found or already processed'
# Process items in parallel
- :parallel_for: item
:in:
_step: validate_order.items
:do:
- id: check_inventory
type: MongoDBFindOne
connectionId: products
properties:
query:
_id:
_item: item.product_id
inventory:
$gte:
_item: item.quantity
- :if:
_not:
_step: check_inventory
:then:
:throw: 'Insufficient inventory'
# Try payment processing
- :try:
- id: process_payment
type: AxiosHttp
connectionId: payment-gateway
properties:
data:
amount:
_step: validate_order.total
currency: 'USD'
source:
_payload: payment_token
:catch:
- id: mark_payment_failed
type: MongoDBUpdateOne
connectionId: orders
properties:
filter:
_id:
_payload: order_id
update:
$set:
status: 'payment_failed'
failed_at:
_date: now
- :reject: 'Payment processing failed'
# Update order status
- id: complete_order
type: MongoDBUpdateOne
connectionId: orders
properties:
filter:
_id:
_payload: order_id
update:
$set:
status: 'completed'
payment_id:
_step: process_payment.data.id
completed_at:
_date: now
# Update inventory
- :for: item
:in:
_step: validate_order.items
:do:
- id: reduce_inventory
type: MongoDBUpdateOne
connectionId: products
properties:
filter:
_id:
_item: item.product_id
update:
$inc:
inventory: -1
# Send confirmation email
- id: send_confirmation
type: SendGridMail
connectionId: email-service
properties:
to:
_step: validate_order.customer_email
template_id: 'order_confirmation'
dynamic_template_data:
order_id:
_payload: order_id
total:
_step: validate_order.total
# Return success
- :return:
success: true
order_id:
_payload: order_id
payment_id:
_step: process_payment.data.id
message: 'Order processed successfully'
:if, :for, :parallel, and :try enable sophisticated flows._step.CallApi steps call other endpoints server-side with isolated _step and _payload namespaces.InternalApi endpoints are server-only — callable from other endpoints but not from client pages.CallAPI action is used to invoke APIs from the client with payloads.:return (success) and :reject (user errors).