ToolJet is the open-source low-code builder 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. ToolJet's sample data sources (and its built-in database) are genuinely useful, but they aren't your HTTP API β build against them and everything specific to your REST integration (data source config, pagination bindings, response shapes, retry behavior, error handling) goes unexercised until the day you swap in the real thing.
This guide points ToolJet's REST queries at a hosted, stateful mock instead: real endpoints, persistent writes, and failure modes you control from a URL param. Works the same on ToolJet Cloud's free plan (2 builders, 2 apps as of September 2026) and on a self-hosted Community Edition instance, 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 the query panel, click + Add, pick REST API, method GET, URL:
https://mockbird.mockbird.workers.dev/m/demo/products?limit=20
Run it, drag a Table component onto the canvas, set its Data property to {{queries.getProducts.data}} β done. The response is a bare JSON array, exactly what the Table wants; no transformation needed. Add &q={{components.textinput1.value}} for server-side search across every field, or sortBy/order/field filters the same way β the conventions are json-server compatible.
Planning more than one query? Create a REST API data source (Data Sources page β API β REST API) with Base URL https://mockbird.mockbird.workers.dev/m/demo instead β every query then shares one config (base URL, headers, auth), and cutover to the real backend later is a one-field edit on the data source, not a hunt through every query.
ToolJet has something most tool builders don't: an OpenAPI data source that generates ready-made operations from a spec β you pick an operation from a dropdown instead of typing URLs. Mockbird publishes a live spec for every project:
curl -s https://mockbird.mockbird.workers.dev/m/demo/openapi.json
Paste that JSON into a new OpenAPI data source and every endpoint β list, get-one, create, update, delete, for every resource β appears as a selectable operation. The spec is generated from the project's actual schema, so it's never stale. Bonus for cutover day: the OpenAPI data source lets you configure a different host per environment which takes precedence over the spec's own server β develop against the mock, point production at the real API, same operations.
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":"ToolJet guide test","price":9.99}'
# β {"id":31, "name":"ToolJet 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 ToolJet that's three more queries on the same data source: a POST /products with a JSON body built from your form components, a PATCH /products/{{components.table1.selectedRow.id}}, a DELETE on the same path. Wire each to a Button's On click event, and add an event handler on the write query that runs the list GET again so the table refreshes after every mutation. That refresh-after-mutation wiring is real app logic β and it only gets exercised against an API where mutations actually mutate.
ToolJet's Table component does server-side pagination: enable Server Side Pagination in the Table properties, add a Page changed event that runs the query, and bind the URL to the table's pageIndex. It's 1-based, and so is our _page β they map directly:
https://mockbird.mockbird.workers.dev/m/demo/products?_page={{components.table1.pageIndex}}&_limit=10
For the Total records server side property, ToolJet's own how-to wires up a second query β a SQL COUNT(*) β because most APIs don't tell you the total. Mockbird sends it on every list response:
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)
REST API queries expose response headers on the query's metadata object (hyphenated keys need bracket notation), so Total records server side becomes:
{{queries.getProducts.metadata.response.headers["x-total-count"]}}
One query, no COUNT companion. Run the query once and peek at metadata in the preview pane to see the whole request/response pair β it's also how you check what any real API is sending you.
ToolJet REST queries ship with Retry on network errors enabled by default: up to 3 automatic retries on status 408, 413, 429, 500, 502, 503, 504, 521, 522 or 524. You've probably never seen it fire. Make it visible with a deterministic failure sequence:
# 1st request β 503, 2nd β 503, 3rd and later β real 200
'https://mockbird.mockbird.workers.dev/m/demo/products?mock_seq=503,503,200&mock_seq_key=yourname'
# curl it four times: 503 503 200 200
Put that URL in a ToolJet query and run it once: it comes back green with data. The public demo request inspector shows what really happened β three requests, two 503s, one 200, all within a second. That's the retry machinery working, observed instead of assumed. Toggle Retry on network errors off (per query or per data source) and the same query fails on the first 503 β which is the configuration your error handling should be designed against, deliberately, not by accident.
Error states usually get tested by hoping something breaks. Make it break:
https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_status=404
One gotcha that makes ToolJet different from other tool builders: because of the default retry list above, mock_status=500 (or 503, or 429) doesn't fail once β it fails four times, then errors, several seconds later. For an instant failure use a code that isn't retried: 400, 401, 403 or 404. Then wire the query's failure event to the alert/fallback behavior you actually want, and use mock_status=503 specifically when you want to rehearse the slow retried-then-failed path. Knowing which of your API's error codes get silently retried three extra times is exactly the kind of thing better learned from a mock than from an incident.
curl -s -o /dev/null -w '%{time_total}\n' \
'https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_delay=3000'
# 3.85s
Every query exposes isLoading β ToolJet's own pagination guide binds the Table's Loading state to {{queries.getProducts.isLoading}}. Add ?mock_delay=3000 (up to 5,000 ms) and the skeleton you get for free becomes inspectable for three whole seconds: does the whole page block, or just the table? Do buttons stay clickable mid-refresh? Combine with Β§5: mock_delay applies per request, so a retried 503 sequence with a delay shows you worst-case latency β the number your users feel when the real API has a bad day.
# 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 table refresh fans out into several queries. The 429 comes with a real Retry-After header plus x-ratelimit-limit/remaining/reset, readable from metadata.response.headers. Note 429 is in ToolJet's default retry list, so with retry on, brief limit-trips can heal invisibly (three retries deep) β run the drill both ways and decide which behavior you want before the real API decides for you.
If the API you're mocking wraps lists in an envelope β {"data": [...]} β your Table bindings should be written against that shape from day one, or the cutover breaks them all:
curl -s 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_envelope=data&limit=2'
# β {"data":[ {β¦}, {β¦} ]}
?mock_envelope=data (or a custom JSON template, or a project-wide default) reshapes every list response β so {{queries.getProducts.data.data}} is correct in the mock and in production, and nothing needs rewriting on swap day. Details in the docs.
When a POST "should work" but the API disagrees, the question is what actually went over the wire β after every {{ }} was evaluated. Point the query at a bin:
curl -s -X POST https://mockbird.mockbird.workers.dev/m/demo/hooks/tooljet-run \
-H 'content-type: application/json' -d '{"app":"orders-admin","page":1}'
# β {"ok":true,"caught":"POST /hooks/tooljet-run","body":{β¦},"receivedAt":"β¦"}
The demo's catch-all route echoes what it caught, and every hit β method, path, headers, body β lands in the public demo request inspector. Fun check: ToolJet adds a tj-x-forwarded-for header (the logged-in app user's IP) to every REST request β run a query and watch it show up in the inspector, along with whatever your data source config really appended. On your own project, one ANY /* custom route is a private bin with a per-project inspector.
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 OpenAPI data source the same way. Mock JWT auth is available when the app needs a login story β ToolJet's REST data source supports Bearer tokens natively, so the mock login flow plugs straight into its auth config. Projects have a 10,000 requests/day cap β app building barely dents it.
Fair's fair: if your data already lives in a database you own, connect ToolJet's native database data sources directly β that's the product's home turf, and no mock is needed because there's no HTTP API boundary to mock. The built-in ToolJet Database is a real answer when the app is the system of record. Reach for a hosted mock when the finished app will talk to a REST API: then the data source config, pagination bindings, response shapes, headers, retry behavior, 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 Budibase version, the Power Automate version, the n8n version, testing loading and error states, simulating rate limits, mock servers from OpenAPI specs, and mocking third-party APIs generally.
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, seq 503β503β200β200, mock_status 404, 3.85s measured delay, 200 200 200 429 rate-limit run with retry-after present, envelope {"data":[β¦]}, bin echo + inspector log) were run against production on 20 Sep 2026 before publishing. ToolJet behavior β the REST API data source's Base URL/headers/auth config, the OpenAPI data source generating operations from a pasted JSON/YAML spec with per-environment host override, {{queries.<name>.data}} and metadata.request/response (bracket notation for hyphenated headers), Table server-side pagination via 1-based pageIndex + Page-changed event + Total-records-via-COUNT-query in their own how-to, Retry on network errors defaulting to 3 retries on 408/413/429/500/502/503/504/521/522/524 with per-query and per-data-source toggles, isLoading, and the tj-x-forwarded-for header β is from ToolJet's official docs (3.16-LTS line) as of September 2026; free-plan shape (2 builders, 50 end users, 2 apps) from their published pricing as of the same date. We don't run your ToolJet instance; if a step here doesn't work, that's a bug: tell us.