Skip to main content
Examples

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 example https://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 as https://YOUR-API-HOSTNAME; replace that with your endpoint. restward list prints 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:

Shell
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

Shell
mkdir restward-consumer-function
cd restward-consumer-function
npm init -y

Note: Node.js 18+ (used by AWS Lambda, Vercel, Netlify) includes global fetch — no additional packages required.

2. The function (AWS Lambda handler)

JavaScript
// 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 }),
    };
  }
};

Offset pagination can skip or duplicate rows if data changes while you page. Keyset pagination is more robust.

JavaScript
// 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

Shell
# 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.json

Next.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)

JavaScript
// 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.

JavaScript
// 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:

JavaScript
// 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 action

Warning: 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

Shell
dotnet new console -n RestWardConsumer
cd RestWardConsumer

2. Add required packages

No additional packages are required for this example.

3. Retry handler

C#
// 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

C#
// 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

Shell
dotnet build
dotnet run

Reconnecting to server...

Please wait while we restore your connection

Connection lost

Retrying in seconds...

Unable to reconnect

The server connection has been lost.
You may need to log in again.

Session Paused

The server has temporarily paused your session.

Session Expired

Your session could not be resumed.
Please reload the page to continue.