Appsmith is the open-source way to build 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. Appsmith's sample datasources are genuinely useful for learning widgets, but they aren't your HTTP API β build against them and everything specific to your REST integration (datasource config, pagination bindings, response shapes, error handling) goes unexercised until the day you swap in the real thing.
This guide points Appsmith'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 Appsmith Cloud's free plan 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. Add a query, pick the REST API datasource type, method GET, URL:
https://mockbird.mockbird.workers.dev/m/demo/products?limit=20
Run it, drop a Table widget, set its Table data to {{ get_products.data }} β done. The response is a bare JSON array, exactly what the Table wants; no transformation needed. Add ?q={{ Table1.searchText }} 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 an Authenticated API datasource with base URL https://mockbird.mockbird.workers.dev/m/demo instead β every query then shares one config (root URL, headers, auth), and cutover to the real backend later is a one-field edit on the datasource, not a hunt through every query.
Appsmith has a cURL Import: create datasource β cURL, paste a command, and it becomes a configured query β method, URL, headers, body, all filled in. That makes this page (and most API docs) directly executable. Try it with:
curl -X POST 'https://mockbird.mockbird.workers.dev/m/demo/products' \
-H 'content-type: application/json' \
-d '{"name":"Appsmith guide test","price":9.99}'
Import, run, and you have a working POST query to rename and rebind. Every curl in our docs works the same way β the fastest path from "reading about an endpoint" to "query in my app" that any tool builder ships.
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":"Appsmith guide test","price":9.99}'
# β {"id":31, "name":"Appsmith 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 Appsmith that's three more queries on the same datasource: a POST /products with a JSON body of {{ create_form.data }}, a PATCH /products/{{ Table1.selectedRow.id }}, a DELETE on the same path. Wire each to a button's onClick, and in the query's onSuccess callback run the list GET so the table refreshes after every write. That refresh-after-mutation wiring is real app logic β and it only gets exercised against an API where mutations actually mutate. (JSON bodies pass through Smart JSON Substitution, which usually casts {{ }} bindings into correct JSON β Β§7's request bin shows you exactly what it produced.)
Appsmith's Table widget does server-side pagination in offset mode ("Paginate with Table Page No"): enable the Server side pagination property, set onPageChange to run the query, and bind the query's URL to the table's reference properties. Table1.pageNo is 1-based, and so is our _page β they map directly:
https://mockbird.mockbird.workers.dev/m/demo/products?_page={{Table1.pageNo}}&_limit={{Table1.pageSize}}
For the Total records property (which turns the pager into real numbered pages), Appsmith'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)
Response headers are exposed on the query's responseMeta object, so Total records becomes:
{{ get_products.responseMeta.headers['x-total-count'] }}
One query, no COUNT companion. (Run the query once and peek at {{ get_products.responseMeta }} in the response pane first β depending on your Appsmith version, header values can arrive as a string or a single-element array; if you see ["30"], append [0] to the binding.) Also worth knowing: Appsmith caps query responses at 5 MB β one more reason to paginate on the server instead of fetching everything and letting the widget slice it.
Queries have onSuccess/onError callbacks β the showAlert toast, the fallback state, the "retry" button you added. They usually get tested by hoping something breaks. Make it break:
https://mockbird.mockbird.workers.dev/m/demo/products?mock_status=500
Run the query and the error path actually executes; remove the param and the same query is green again. One URL parameter is the whole toggle. mock_status takes any code (try 404 on the single-record GET behind your detail drilldown). For "fails twice then recovers" β the case a retry button should survive β use a deterministic 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=myapp'
# run four times: 503 503 200 200
curl -s -o /dev/null -w '%{time_total}\n' \
'https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_delay=3000'
# 3.79s
Appsmith times out queries at 10,000 ms by default (adjustable per query in Settings, up to 60,000 ms; self-hosted instances can go beyond via the APPSMITH_SERVER_TIMEOUT env var). Set a query's timeout to 2000 against a 3-second mock_delay and the timeout error fires on demand β so you can decide what the app should do when the real API has a bad day, and check the loading states of every widget bound to the query while it hangs. mock_delay goes up to 5,000 ms.
# 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=myapp&limit=1'
# 4 requests β 200 200 200 429
Internal tools are rate-limit magnets β one table refresh fans out into several queries, and a JS loop over selected rows will hammer any API. The 429 comes with a real Retry-After header plus x-ratelimit-limit/remaining/reset, all readable from responseMeta.headers β so you can build the "back off and tell the user" behavior now instead of discovering the need in production. Use your own mock_ratelimit_key so parallel testers don't share a counter.
When a POST "should work" but the API disagrees, the question is what actually went over the wire β after every {{ }} was evaluated and Smart JSON Substitution did its casting. Point the query at a bin:
curl -s -X POST https://mockbird.mockbird.workers.dev/m/demo/hooks/appsmith-run \
-H 'content-type: application/json' -d '{"app":"orders-admin","page":1}'
# β {"ok":true,"caught":"POST /hooks/appsmith-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. Useful for checking binding evaluation, number-vs-string coercion in body fields, and which headers your datasource config really adds. On your own project, one ANY /* custom route is a private bin with a per-project inspector.
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 {{ get_products.data.data }} is correct in the mock and in production, and nothing needs rewriting on swap day. Details in the docs.
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 Β§2's cURL Import accepts your project's curls the same way. Mock JWT auth is available when the app needs a login story (it plugs into the Authenticated API datasource's bearer-token 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 Appsmith's native database datasources directly β that's the product's home turf, and no mock is needed because there's no HTTP API boundary to mock. The sample datasources are fine for learning widgets. Reach for a hosted mock when the finished app will talk to a REST API: then the datasource config, pagination bindings, response shapes, headers, 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 Bubble version, the ToolJet version, the Budibase version, the Power Automate version, the Zapier version, the n8n version, testing loading and error states, simulating rate limits, request bins compared, and mocking third-party APIs generally.
Verification: all demo curls on this page (limit=2 list, POSTβGET-backβDELETE with id 31, _page=2&_limit=5 returning records 6β10 with x-total-count: 30, mock_status 500, 3.79s measured delay, seq 503β503β200β200, 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. Appsmith behavior β the REST API vs Authenticated API datasource split, cURL Import, offset pagination via pageSize/pageNo/pageOffset with the Server-side-pagination toggle and onPageChange, the Total-records-via-separate-COUNT-query pattern in their own how-to, responseMeta exposing status and headers, the 10s default / 60s max query timeout and APPSMITH_SERVER_TIMEOUT, Smart JSON Substitution defaults, and the 5 MB response cap β is from Appsmith's official docs and issue tracker as of September 2026; we don't run your Appsmith instance, and header-value shape in responseMeta can vary by version (the guide says how to check). If a step here doesn't work, that's a bug: tell us.