SurveyJS Server Integration · PHP / Laravel I.5 · Choices from the web
You work in a private copy of the demo database, kept until it has been unused for 24 hours.

I.5

Choices from the web

You need this: Lists that live elsewhere: countries, products, users, offices.

A dropdown, checkbox or radio group loads its options from a URL with choicesByUrl, from a public web service or your own API. The URL can include answers and variables in curly braces, such as /api/offices?region={region}, and the list reloads when they change. The browser calls the URL directly, so a third-party service must allow cross-origin requests (CORS). If it doesn't, or it needs a secret key, call it through your own server, as /api/countries does. To send authentication headers, use settings.web.onBeforeRequestChoices.

This step on the Server Integration page Source on GitHub

restcountries.com retired its keyless v3.1 API, which now answers {"success": false}. The route treats that like an outage: it logs a warning and serves shared/seed/countries.json uncached, so the log shows fallback.

Try this

  1. In Find an office, change the Region: a new GET /api/offices?region=… appears in Requests, and the Office list reloads.
  2. Open the Country list: it comes from GET /api/countries, the server's proxy. The log shows X-Demo-Cache: miss when the server fetched restcountries.com, hit from the 24-hour cache, fallback when the service failed and the bundled list was served (demo-only header).
  3. Every choices request carries Authorization: Bearer demo-token, added by onBeforeRequestChoices.

Code that just ran

These regions are read from the files that served this page, the same lines the Server Integration page shows.

Client · shared/client/choices-from-web.js · sjs:I.5.client View on GitHub
// In the definition — no code (shared/definitions/choices-from-web.json):
//   "choicesByUrl": { "url": "/api/offices?region={region}", "valueName": "id", "titleName": "name" }
//   "choicesByUrl": { "url": "/api/countries", "valueName": "code", "titleName": "name" }
// {region} is replaced with the answer to "region" (or a variable), and the list reloads when it changes.

// Optional: add your auth header to every choices request
import { settings } from "survey-core";
settings.web.onBeforeRequestChoices = (_, options) => {
  options.request?.setRequestHeader("Authorization", `Bearer ${token}`);
};
Server · routes/examples/choices-from-web.php · sjs:I.5.server View on GitHub
// GET /api/offices?region= — {region} from the form arrives as a query parameter
Route::get('/api/offices', function (Request $request) {
    return DB::table('offices')->where('region', $request->query('region', ''))->orderBy('name')->get(['id', 'name']);
});

// GET /api/countries — a proxy: the browser can't call a service without CORS, your server can
Route::get('/api/countries', function () {
    try {
        $countries = Cache::remember('countries', now()->addDay(), fn () => Http::timeout(5)
            ->get('https://restcountries.com/v3.1/all', ['fields' => 'name,cca2'])->throw()->collect()
            ->map(fn (array $country) => ['code' => $country['cca2'], 'name' => $country['name']['common']])
            ->sortBy('name')->values()->all());
    } catch (Throwable $e) {   // down, or a different answer: serve the bundled list for 10 minutes, so requests don't each wait for the timeout
        Log::warning('restcountries.com failed; serving shared/seed/countries.json', ['error' => $e->getMessage()]);
        $countries = json_decode(file_get_contents(base_path('shared/seed/countries.json')), true);
        Cache::put('countries', $countries, now()->addMinutes(10));
    }

    return response()->json($countries)->header('Cache-Control', 'public, max-age=86400');
});

Definition: shared/definitions/choices-from-web.json