Budibase is the open-source low-code platform for internal tools β and it meets the same chicken-and-egg as every tool builder: the app is wanted now, the API it should sit on ships next sprint. Or the API exists, and pointing a half-built admin panel with a working Delete button at production data is how incidents happen. Budibase's internal database is genuinely useful, but it isn't your HTTP API β build against it and everything specific to your REST integration (connection config, pagination mapping, bindings, response shapes, error handling) goes unexercised until the day you swap in the real thing.
This guide points Budibase's REST queries at a hosted, stateful mock instead: real endpoints, persistent writes, and failure modes you control from a URL param. It works the same on a self-hosted Community/Free instance (unlimited apps and users, the usual home for Budibase as of September 2026 β Budibase Cloud's plans start paid with a 14-day trial) β and Mockbird needs no signup at all. Every curl below was verified against production before publishing.
The public demo project is a seeded e-commerce API. In Budibase, REST lives in Workspace Settings β Connections β APIs: add a connection, set the Base URL to:
https://mockbird.mockbird.workers.dev/m/demo
then Open in API Editor, create a query with method GET and path /products?limit=20, and click Send. The response is a bare JSON array β the response view shows the status, timing, and the schema Budibase generated from it, and no transformer is needed. Bind it to a table or repeater and you have live data. Because base URL, shared headers, and auth live on the connection, cutover to the real backend later is a one-field edit β not a hunt through every query.
Budibase's REST query import creates multiple queries in one go from an OpenAPI 2.0/3.0 spec (JSON or YAML, file or pasted raw text). Mockbird publishes a live spec for every project:
curl -s https://mockbird.mockbird.workers.dev/m/demo/openapi.json
Paste that JSON into the importer and list, get-one, create, update, and delete queries for every resource appear at once. The spec is generated from the project's actual schema, so it's never stale. Worth knowing: Budibase's own docs say Postman collections must be converted with postman2openapi before import β Mockbird speaks OpenAPI natively, so there's nothing to convert (and if you also live in Postman, every project exports a native Postman collection too).
The Create/Edit/Delete half of an admin panel is the half you can't build against read-only fake APIs. Here writes are real:
curl -s -X POST https://mockbird.mockbird.workers.dev/m/demo/products \
-H 'content-type: application/json' \
-d '{"name":"Budibase guide test","price":9.99}'
# β {"id":31, "name":"Budibase guide test", "price":9.99}
curl -s https://mockbird.mockbird.workers.dev/m/demo/products/31 # it's really there
curl -s -X DELETE https://mockbird.mockbird.workers.dev/m/demo/products/31
In Budibase that's three more queries on the same connection β a POST /products with a body built from form bindings, a PATCH /products/{{ id }}, a DELETE on the same path β each wired to a button's Execute query action, with the list query re-run afterwards so the table refreshes. That refresh-after-mutation wiring is real app logic, and it only gets exercised against an API where mutations actually mutate.
Budibase's REST pagination supports a page-number model: you map the request's page and page-size field names, then click Send repeatedly to validate progression. Mockbird's list endpoints take _page (1-based) and _limit β map those two names and paging works:
curl -si 'https://mockbird.mockbird.workers.dev/m/demo/products?_page=2&_limit=5' | grep -i x-total-count
# x-total-count: 30 (body: records 6β10)
Budibase's docs describe typical page-number responses as carrying fields like page, totalPages, or hasNext in the body β which most APIs (and most mocks) don't give you. Mockbird can reshape any list response server-side with mock_envelope. This URL-encoded template:
# template: {"data":"$data","page":"$page","total":"$total","hasNext":"$hasMore"}
curl -s 'https://mockbird.mockbird.workers.dev/m/demo/products?_page=1&_limit=10&mock_envelope=%7B%22data%22%3A%22%24data%22%2C%22page%22%3A%22%24page%22%2C%22total%22%3A%22%24total%22%2C%22hasNext%22%3A%22%24hasMore%22%7D'
# β {"data":[β¦10 recordsβ¦], "page":1, "total":30, "hasNext":true}
# same URL with _page=3 β "hasNext": false
β exactly the metadata shape Budibase's pagination mapper (or your own next/prev button logic) expects, with no transformer script. Set it as a project-wide default and every list query gets the envelope without the query param.
Budibase's cursor model asks you to map "the API field that returns the next cursor token", and its troubleshooting matrix warns that non-deterministic API ordering causes duplicate or missing rows. Rehearse the whole thing against a real, deterministic cursor API:
curl -s 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_cursor=1&limit=10'
# β {"data":[β¦10β¦], "next_cursor":"eyJ2IjoxLCJvIjoxMH0β¦", "has_more":true}
# follow it: ?cursor=<that token>&limit=10 β records 11β20, then 21β30 with has_more:false
# tamper with it: ?cursor=garbage β HTTP 400
Map Budibase's cursor response path to next_cursor, click Send repeatedly, and watch first/next/final page behave β ordering is stable, so the duplicate-rows failure mode their docs warn about can't muddy the test. The invalid-token 400 is what your error handling should see when a stale cursor comes back after a data reset. More in the cursor pagination guide.
Budibase passes runtime values into queries with handlebars bindings β {{ status }} in a URL, param, header, or body, with a default value for testing. Mockbird gives those bindings something real to hit β every field is an exact-match filter, plus operators and free-text search:
GET /products?category={{ category }} # exact match
GET /products?price_gte={{ minPrice }} # _gte/_lte/_gt/_lt/_ne/_like
GET /products?q={{ search }} # search across every field
GET /products?sortBy={{ sortField }}&order={{ sortOrder }}
Set defaults (all, 0, empty) exactly as their tutorial suggests, hit Send, and confirm the filter narrows β empty-valued params are ignored server-side, so a blank default returns the full list instead of filtering everything away.
Error states usually get tested by hoping something breaks. Make it break:
https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_status=404 # or 401, 403, 500, 503β¦
The API Editor's response view shows the status code on every Send, so you can watch each failure land, then design what the app does about it β an error state on the table, a notification action, a fallback screen β before the real API produces its first surprise 500.
curl -s -o /dev/null -w '%{time_total}\n' \
'https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_delay=3000'
# 3.80s
Add ?mock_delay=3000 (up to 5,000 ms) to any query URL and the loading experience becomes inspectable for three whole seconds: does the whole screen block, or just the table? Are buttons still clickable mid-refresh? The response view's execution time confirms the delay is really happening end-to-end.
# allow 3 requests per 60s for your key, then 429 with Retry-After
'https://mockbird.mockbird.workers.dev/m/demo/products?mock_ratelimit=3&mock_ratelimit_key=yourname&limit=1'
# 4 curls β 200 200 200 429
Internal tools are rate-limit magnets β one screen refresh fans out into several queries. The 429 comes with a real Retry-After header plus x-ratelimit-limit/remaining/reset, so you can decide how the app should behave when the real API starts rationing β before it does.
Budibase's request view shows the final URL and headers β but it deliberately sanitizes the preview: Authorization-type headers are redacted and environment variables display as names, not values. Good security defaults; unhelpful when the API rejects a request and you need the wire truth. Point the query at a bin:
curl -s -X POST https://mockbird.mockbird.workers.dev/m/demo/hooks/budibase-run \
-H 'content-type: application/json' -d '{"app":"orders-admin","binding":"resolved"}'
# β {"ok":true,"caught":"POST /hooks/budibase-run","body":{β¦},"receivedAt":"β¦"}
The demo's catch-all route echoes what it caught, and every hit β method, path, headers, body, after every {{ }} was evaluated β lands in the public demo request inspector. On your own project, one ANY /* custom route is a private bin with a per-project inspector.
Budibase REST connections support Basic and Bearer authentication configs. Mockbird projects come with a mock JWT auth flow β any email/password returns a real signed token:
curl -s -X POST https://mockbird.mockbird.workers.dev/m/demo/auth/login \
-H 'content-type: application/json' \
-d '{"email":"you@example.com","password":"anything"}'
# β {"token":"eyJβ¦", "user":{β¦}}
Drop the token into a Bearer auth config, flip your own project to protected mode, and every query 401s without it β so the app's auth plumbing is exercised long before the real identity provider shows up.
One curl (or one click), no signup:
curl -s -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H "content-type: application/json" \
-d '{"preset":"ecommerce"}'
# β {"id":"abc123xyz9", "adminKey":"β¦", ...}
Presets: blog / ecommerce / saas β or define resources by hand, or import an OpenAPI spec, db.json, CSV, or a HAR recording of the real API. Every simulation param above works on your project's URLs, and your project's openapi.json feeds Β§2's query import the same way. Projects have a 10,000 requests/day cap β app building barely dents it.
Fair's fair: if the app is the system of record, Budibase's internal database is the product's home turf β no HTTP boundary, nothing to mock. Its SQL/NoSQL connectors talk to databases you already own just as directly. Reach for a hosted mock when the finished app will talk to a REST API: then the connection config, pagination mapping, bindings, response shapes, and error handling are the work, and they deserve a target that behaves like an API β including on its bad days.
Related: the Retool version of this guide, the Appsmith version, the ToolJet version, the n8n version, cursor pagination, testing loading and error states, simulating rate limits, and mock servers from OpenAPI specs.
Verification: all demo curls on this page (limit=20 list returning a bare 20-element array, live openapi.json with 8 paths, POSTβGET-backβDELETE with id 31, _page=2&_limit=5 returning records 6β10 with x-total-count: 30, the envelope template returning {"data","page","total","hasNext"} with hasNext:false on page 3, cursor pages 1β2β3 ending has_more:false with a tampered token returning 400, mock_status 404, 3.80s measured delay, 200 200 200 429 rate-limit run with retry-after present, bin echo + inspector log, and the demo login returning a signed JWT) were run against production on 20 Sep 2026 before publishing. Budibase behavior β REST connections under Workspace Settings β Connections β APIs with connection-level base URL/headers/auth (Basic/Bearer/OAuth2), the API Editor's Send/response-view/request-view with sanitized credential preview, query import accepting OpenAPI 2.0/3.0 JSON/YAML by file or raw text with Postman collections requiring postman2openapi conversion, handlebars {{ binding }} syntax with defaults, and pagination's page-number/offset/cursor models with "Send repeatedly to validate progression" and the duplicate-rows-from-unstable-sort warning β is from Budibase's official docs as of September 2026; plan shape (self-host Free with unlimited apps/users; cloud plans from $19/mo with 14-day trials) from their published pricing as of the same date. We don't run your Budibase instance; if a step here doesn't work, that's a bug: tell us.