dimah-survey

Quickstart

Build a complete SurveyJS response flow in Next.js.

This guide creates the smallest complete flow: publish a SurveyJS document, start a response, render its frozen definition, save page progress, and submit it.

This quickstart uses process-local memory and a fixed respondent. Before production, use a durable store, derive identity from the session, and guard the editor route.

Install

npm i @dimah-survey/server @dimah-survey/react survey-core survey-react-ui

survey-react-ui renders questions. dimah-survey owns only the lifecycle around the SurveyJS document.

Create the server

Create one instance for respondents and one for trusted editor operations. They must share the same store.

lib/survey.ts
import {
  SURVEY_API_BASE_PATH,
  SURVEY_EDITOR_API_BASE_PATH,
  dimahSurvey,
  guardRespondent,
  memoryAdapter,
} from "@dimah-survey/server";

const database = memoryAdapter();

export const editor = dimahSurvey({
  audience: "editor",
  database,
  basePath: SURVEY_EDITOR_API_BASE_PATH,
});

export const fill = dimahSurvey({
  audience: "fill",
  database,
  basePath: SURVEY_API_BASE_PATH,
  guard: guardRespondent("demo"),
});

The fill guard stamps "demo" onto every response operation. With the default "one-open" policy, another start resumes the open draft instead of creating a second one.

Mount both audiences

app/api/survey/[[...path]]/route.ts
import { toNextJsHandler } from "@dimah-survey/server/next";
import { fill } from "@/lib/survey";

export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(fill);
app/api/admin/survey/[[...path]]/route.ts
import { toNextJsHandler } from "@dimah-survey/server/next";
import { editor } from "@/lib/survey";

export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(editor);

The paths match the default base paths: /api/survey for fill and /api/admin/survey for editor. See Mount the server for other runtimes.

Publish a SurveyJS document

Use the in-process editor API from server code. Saving changes draftJson; publishing explicitly copies that draft to publishedJson.

lib/welcome.ts
import { isAPIError } from "@dimah-survey/server";
import { editor } from "@/lib/survey";

const definition = {
  title: "Welcome",
  pages: [
    {
      name: "welcome",
      elements: [
        { type: "text", name: "name", title: "Your name", isRequired: true },
        {
          type: "rating",
          name: "score",
          title: "How was it?",
          isRequired: true,
        },
      ],
    },
  ],
};

export async function ensureWelcomeSurvey() {
  try {
    await editor.api.getSurvey({ query: { id: "welcome" } });
  } catch (error) {
    if (!isAPIError(error) || error.code !== "SURVEY_NOT_FOUND") throw error;

    await editor.api.saveSurvey({
      body: { id: "welcome", slug: "welcome", draftJson: definition },
    });
    await editor.api.publishSurvey({ body: { id: "welcome" } });
  }
}

getSurvey throws SURVEY_NOT_FOUND when the row is missing; it does not return null.

Create the browser client

lib/fill-client.ts
"use client";

import { createFillClient } from "@dimah-survey/react";

export const fillClient = createFillClient({
  baseURL: "/api/survey",
});

Browser clients take flat input objects. In-process APIs use { body } or { query }.

Render the response snapshot

components/fill.tsx
"use client";

import dynamic from "next/dynamic";
import { useSurveyResponse } from "@dimah-survey/react";
import { fillClient } from "@/lib/fill-client";

import "survey-core/survey-core.min.css";

const Survey = dynamic(
  () => import("survey-react-ui").then((mod) => mod.Survey),
  { ssr: false },
);

export function Fill({ responseId }: { responseId: string }) {
  const { model, error, saveError, stale, reload } = useSurveyResponse({
    client: fillClient,
    responseId,
  });

  if (error) return <p>{error.message}</p>;
  if (!model) return null;

  return (
    <>
      {saveError ? <p>{saveError.message}</p> : null}
      {stale ? <button onClick={reload}>Reload</button> : null}
      <Survey model={model} />
    </>
  );
}

The hook loads response.definition, constructs a Model, and binds partial save and submit. It never renders UI. partial defaults to "page", so moving to the next page persists the complete survey.data object.

Start the response

app/page.tsx
import { Fill } from "@/components/fill";
import { fill } from "@/lib/survey";
import { ensureWelcomeSurvey } from "@/lib/welcome";

export default async function Page() {
  await ensureWelcomeSurvey();
  const response = await fill.api.startResponse({
    body: { surveyId: "welcome" },
  });

  return <Fill responseId={response.id} />;
}

The first request creates a draft whose definition is a copy of the published document. Refreshing resumes that draft. A later publish cannot rewrite it.

Make it production-ready

  • Replace memoryAdapter() with the SQL store or your own durable SurveyStore. See Persistence.
  • Read the respondent from your session and protect the editor audience. See Security.
  • Add Creator autosave without coupling it to publish. See Survey Creator.

On this page