Calling a RestWard API from your app
Complete walkthroughs for calling a generated RestWard API from a serverless function, a Next.js app, and a .NET service — including secrets handling, retries, and pagination.
This guide shows you how to consume a generated RestWard API from three common environments: a serverless function, a Next.js app, and a .NET service.
Every example uses the same API contract:
| Concern | How RestWard does it | Your code should ... |
|---|---|---|
| Base URL | Your API's endpoint from the API's detail page in the dashboard (a hostname of its own, such as https://chinook-test-org-vzubqp.run.restward.tech) + /api/{schema}/{table} |
Use an environment variable; never hard-code |
| Authentication | X-API-Key header, or Authorization: Bearer for JWT, or client cert for mTLS |
Send one credential per request; never expose a key in browser JS |
| Pagination | limit (1-1000, default 100), offset, sort, fields, count |
Page with limit + offset or keyset (column[gt]=value) |
| Errors | {"error": "..."} JSON body; 429 includes Retry-After |
Check status first, then read error; back off on 429 |
| Rate limits | 429 Too Many Requests with Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Scope |
Honor Retry-After; never retry other 4xx |
For the authentication, rate limiting, and API key mechanics, see the reference pages:
Prerequisites
- A generated RestWard API (you have the API's schema name and table names)
- An API key for that API (created under Auth & Keys in the RestWard dashboard)
- Your API's base URL (its endpoint). Open the API in the dashboard: its detail page shows the
endpoint, a hostname of its own in the form
https://{api-slug}-{org-slug}-{suffix}.run.restward.tech(for examplehttps://chinook-test-org-vzubqp.run.restward.tech). The suffix is six characters derived from your organization, and long slugs are shortened so the name fits. The examples on this page write it ashttps://YOUR-API-HOSTNAME; replace that with your endpoint.restward listprints it too.
Never put an API key in browser-side code. API keys grant full access to your data. If a key must be used from a browser, proxy the request through your backend service.
Environment variables
All examples read from the same set of environment variables:
export RESTWARD_API_URL="https://YOUR-API-HOSTNAME"
export RESTWARD_API_KEY="rw_..."In production: Use your platform's secret manager (AWS Secrets Manager, Azure Key Vault, GitHub Secrets, Vercel/Netlify environment variables, etc.) — never commit keys to source control.
Serverless function (JavaScript / AWS Lambda, Vercel, Netlify)
A serverless function is the simplest consumer: it runs on the server, so it can safely hold an API key in its environment, and it can retry on 429 without exposing the key to a browser.
1. Set up the project
mkdir restward-consumer-function
cd restward-consumer-function
npm init -yNote: Node.js 18+ (used by AWS Lambda, Vercel, Netlify) includes global
fetch— no additional packages required.
2. The function (AWS Lambda handler)
// index.mjs (ES modules)
// Uses global fetch (Node.js 18+)
const apiUrl = process.env.RESTWARD_API_URL;
const apiKey = process.env.RESTWARD_API_KEY;
// Retry with exponential backoff for 429 and 5xx
async function fetchWithRetry(url, options = {}, retries = 3, backoffMs = 1000) {
for (let attempt = 1; attempt <= retries; attempt++) {
const res = await fetch(url, {
...options,
headers: { ...options.headers, 'X-API-Key': apiKey },
});
// 429: honor Retry-After (seconds) or use exponential backoff
if (res.status === 429) {
const retryAfter = Number(res.headers.get('Retry-After') ?? 0) * 1000;
const delay = retryAfter > 0 ? retryAfter : backoffMs * Math.pow(2, attempt - 1);
if (attempt < retries) {
await new Promise(r => setTimeout(r, delay));
continue;
}
}
// Do NOT retry other 4xx (they fail the same way)
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
return res;
}
// 5xx: retry with backoff
if (res.status >= 500 && attempt < retries) {
await new Promise(r => setTimeout(r, backoffMs * Math.pow(2, attempt - 1)));
continue;
}
return res;
}
return null;
}
// Example: list artists with offset pagination
export const handler = async (event) => {
try {
const limit = 100;
let offset = 0;
const allArtists = [];
while (true) {
const url = new URL(`/api/public/artist?limit=${limit}&offset=${offset}&sort=artist_id`, apiUrl);
const res = await fetchWithRetry(url);
if (!res?.ok) {
const body = await res?.json().catch(() => ({}));
return {
statusCode: res?.status ?? 500,
body: JSON.stringify({ error: body?.error ?? 'Request failed' }),
};
}
const artists = await res.json();
allArtists.push(...artists);
// Fewer than limit = last page
if (artists.length < limit) break;
offset += limit;
}
return {
statusCode: 200,
body: JSON.stringify(allArtists),
};
} catch (err) {
return {
statusCode: 500,
body: JSON.stringify({ error: err.message }),
};
}
};3. Keyset pagination (recommended for changing data)
Offset pagination can skip or duplicate rows if data changes while you page. Keyset pagination is more robust.
// Keyset pagination: page through artists by artist_id
async function listAllArtistsKeyset() {
const limit = 100;
let lastId = 0;
const allArtists = [];
while (true) {
const url = new URL(
`/api/public/artist?artist_id[gt]=${lastId}&sort=artist_id&limit=${limit}`,
apiUrl
);
const res = await fetchWithRetry(url);
if (!res?.ok) throw new Error(`HTTP ${res?.status}`);
const artists = await res.json();
allArtists.push(...artists);
if (artists.length < limit) break;
lastId = artists.at(-1).artist_id;
}
return allArtists;
}4. Deploy and test
# AWS Lambda
zip function.zip index.mjs
aws lambda create-function --function-name RestWardConsumer \
--runtime nodejs20.x --handler index.handler --zip-file fileb://function.zip \
--environment Variables="{RESTWARD_API_URL=https://YOUR-API-HOSTNAME,RESTWARD_API_KEY=rw_...}"
# Test
aws lambda invoke --function-name RestWardConsumer out.json && cat out.jsonNext.js app (App Router)
In a Next.js app you must not call RestWard directly from client components — the API key would be exposed. Instead, call RestWard from Server Components or Route Handlers (server-side), or create a proxy API route.
Option A: Server Component (direct, server-side only)
// app/artists/page.tsx
// This runs on the server; the key stays on the server
async function fetchArtists() {
const apiUrl = process.env.RESTWARD_API_URL;
const apiKey = process.env.RESTWARD_API_KEY;
const res = await fetch(`${apiUrl}/api/public/artist?limit=50&sort=name`, {
headers: { 'X-API-Key': apiKey },
cache: 'no-store',
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error ?? `HTTP ${res.status}`);
}
return res.json();
}
export default async function ArtistsPage() {
const artists = await fetchArtists();
return (
<main>
<h1>Artists</h1>
<ul>
{artists.map(a => (
<li key={a.artist_id}>{a.name}</li>
))}
</ul>
</main>
);
}Option B: Route Handler (API endpoint that proxies to RestWard)
This is the recommended pattern for Next.js: your frontend calls your own API route, which adds the key and forwards to RestWard.
// app/api/artists/route.ts
import { NextResponse } from 'next/server';
const apiUrl = process.env.RESTWARD_API_URL;
const apiKey = process.env.RESTWARD_API_KEY;
// Retry helper
async function fetchWithRetry(url, options = {}, retries = 3, backoffMs = 1000) {
for (let attempt = 1; attempt <= retries; attempt++) {
const res = await fetch(url, {
...options,
headers: { ...options.headers, 'X-API-Key': apiKey },
});
if (res.status === 429) {
const retryAfter = Number(res.headers.get('Retry-After') ?? 0) * 1000;
const delay = retryAfter > 0 ? retryAfter : backoffMs * Math.pow(2, attempt - 1);
if (attempt < retries) {
await new Promise(r => setTimeout(r, delay));
continue;
}
}
if (res.status >= 500 && attempt < retries) {
await new Promise(r => setTimeout(r, backoffMs * Math.pow(2, attempt - 1)));
continue;
}
return res;
}
return null;
}
export async function GET(request) {
const { searchParams } = new URL(request.url);
const limit = searchParams.get('limit') ?? '50';
const offset = searchParams.get('offset') ?? '0';
const sort = searchParams.get('sort') ?? 'name';
const url = new URL(
`/api/public/artist?limit=${limit}&offset=${offset}&sort=${sort}`,
apiUrl
);
const res = await fetchWithRetry(url);
if (!res) {
return NextResponse.json({ error: 'Request failed after retries' }, { status: 502 });
}
const body = await res.json();
return NextResponse.json(body, { status: res.status });
}Now your client components can safely call /api/artists without exposing the key:
// app/artists/client-page.tsx
'use client';
async function loadArtists() {
const res = await fetch('/api/artists?limit=50');
return res.json();
}
// use loadArtists in useEffect or a form actionWarning: Never call RestWard directly from client-side JavaScript with an API key. Always proxy through a server route.
.NET service (Console app, Worker, or ASP.NET Core)
A .NET service can use HttpClient with a delegating handler for retries and a base address from configuration.
1. Set up the project
dotnet new console -n RestWardConsumer
cd RestWardConsumer2. Add required packages
No additional packages are required for this example.
3. Retry handler
// RetryHandler.cs
using System.Net;
public sealed class RetryHandler : DelegatingHandler
{
private readonly int _maxRetries;
public RetryHandler(int maxRetries)
{
_maxRetries = maxRetries;
InnerHandler = new HttpClientHandler();
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage? response = null;
for (var attempt = 1; attempt <= _maxRetries; attempt++)
{
response = await base.SendAsync(request, cancellationToken);
// 429: honor Retry-After header
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
if (response.Headers.RetryAfter?.Delta.HasValue == true)
{
var delay = response.Headers.RetryAfter.Delta.Value;
if (attempt < _maxRetries)
{
response.Dispose();
await Task.Delay(delay, cancellationToken);
continue;
}
}
else if (attempt < _maxRetries)
{
response.Dispose();
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)), cancellationToken);
continue;
}
}
// Do NOT retry other 4xx
if (response.StatusCode >= HttpStatusCode.BadRequest &&
response.StatusCode < HttpStatusCode.InternalServerError &&
response.StatusCode != HttpStatusCode.TooManyRequests)
{
return response;
}
// 5xx: retry with backoff
if (response.StatusCode >= HttpStatusCode.InternalServerError && attempt < _maxRetries)
{
response.Dispose();
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)), cancellationToken);
continue;
}
return response;
}
return response!;
}
}4. Consumer code
// Program.cs
using System.Text.Json;
var apiUrl = Environment.GetEnvironmentVariable("RESTWARD_API_URL") ?? "https://YOUR-API-HOSTNAME";
var apiKey = Environment.GetEnvironmentVariable("RESTWARD_API_KEY");
if (string.IsNullOrEmpty(apiKey))
{
Console.Error.WriteLine("RESTWARD_API_KEY is required.");
return 1;
}
var httpClient = new HttpClient(new RetryHandler(3))
{
BaseAddress = new Uri(apiUrl),
};
httpClient.DefaultRequestHeaders.Add("X-API-Key", apiKey);
try
{
// List all artists with keyset pagination
var allArtists = await ListAllArtistsAsync(httpClient, "public", "artist", "artist_id");
Console.WriteLine($"Loaded {allArtists.Count} artists");
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
return 1;
}
return 0;
// Keyset pagination helper
async Task<List<JsonElement>> ListAllArtistsAsync(HttpClient client, string schema, string table, string keyColumn, int limit = 1000)
{
var all = new List<JsonElement>();
long? lastId = null;
while (true)
{
var url = $"/api/{schema}/{table}?sort={keyColumn}";
if (lastId.HasValue)
url += $"&{keyColumn}[gt]={lastId}";
url += $"&limit={limit}";
var rows = await client.GetFromJsonAsync<List<JsonElement>>(url) ?? [];
all.AddRange(rows);
if (rows.Count < limit)
break;
lastId = rows[^1].GetProperty(keyColumn).GetInt64();
}
return all;
}5. Build and test
dotnet build
dotnet run