Skip to content

CRUD workflows

Common content-management workflows — creating, listing, updating, organizing, and bulk-importing questions and categories.

Updated 2026-05-20

The day-to-day patterns for managing FAQ content. Every example uses @faqapp/core; the equivalent cURL is in the API reference.

Setup

import { createFAQClient } from "@faqapp/core";

const faq = createFAQClient({
  apiKey: process.env.FAQAPP_API_KEY!,
  organizationSlug: "acme"
});

The client wraps every endpoint with typed methods, automatic retries on transient errors, and the typed error classes from Errors.

Create a question

const question = await faq.questions.create({
  title: "How do I cancel my subscription?",
  answer: "Open Settings → Billing → Cancel subscription. You keep access through the end of the billing period.",
  categorySlug: "billing",
  status: "published"
});

console.log(question.id);    // "q_8X3F"
console.log(question.slug);  // "how-do-i-cancel-my-subscription"

categorySlug is a convenience over categoryId — pass either. Slug auto-derives from title unless you override with slug:.

For idempotent writes (deduping at retry time), pass idempotencyKey:

await faq.questions.create(
  { title: "…", answer: "…", categorySlug: "billing" },
  { idempotencyKey: "import-row-42" }
);

List with cursor pagination

const { data, meta } = await faq.questions.list({
  status: "published",
  limit: 50
});

console.log(data.length);              // up to 50
console.log(meta.pagination.hasMore);  // true if there's a next page

Walk every page:

let cursor: string | null = null;
const all: Question[] = [];

do {
  const page = await faq.questions.list({ limit: 100, cursor });
  all.push(...page.data);
  cursor = page.meta.pagination.cursor;
} while (cursor);

console.log(`fetched ${all.length} questions`);

Don’t parallelize page fetches — cursors are server-side state; fan-out is order-undefined.

Update a question

const updated = await faq.questions.update("q_8X3F", {
  answer: "Updated answer. Each update creates a revision (max 50 per question)."
});

Partial — only the fields you pass are touched. Every update writes a revision; the most recent 50 are kept per question. See Revisions below.

Delete a question

await faq.questions.delete("q_8X3F");
// 204 No Content. Soft delete; purged after 30 days.

Returns nothing on success. The NotFoundError class is thrown if the id doesn’t exist.

Create + organize categories

const billing = await faq.categories.create({
  name: "Billing",
  description: "Subscriptions, invoices, refunds.",
  order: 1
});

await faq.categories.create({ name: "API", order: 2 });
await faq.categories.create({ name: "Security", order: 3 });

order controls sort. Lower numbers come first. Use multiples of 10 (10, 20, 30) so you can insert between them later without renumbering everything.

To move a question between categories:

await faq.questions.update("q_8X3F", { categorySlug: "billing" });

Bulk import

For initial migration from CSV or another tool, use the create-many helper:

import { parse } from "csv-parse/sync";
import { readFileSync } from "node:fs";

const rows = parse(readFileSync("faq-export.csv"), { columns: true });

const results = await Promise.allSettled(
  rows.map((row, i) =>
    faq.questions.create(
      {
        title: row.question,
        answer: row.answer,
        categorySlug: row.category
      },
      { idempotencyKey: `import-${row.id}` }
    )
  )
);

const ok = results.filter((r) => r.status === "fulfilled").length;
const failed = results.filter((r) => r.status === "rejected");

console.log(`${ok} created, ${failed.length} failed`);
for (const f of failed) console.error((f as PromiseRejectedResult).reason);

The idempotencyKey makes the whole script safely re-runnable — if it crashes halfway, re-run and only the missing rows actually create.

Revisions and history

Every update creates a revision. Read the history:

const revisions = await faq.questions.revisions("q_8X3F");
// Array<{ revision: number; title: string; answer: string; createdAt: Date }>

Restore an old revision by passing its contents to update:

const r = revisions[5];
await faq.questions.update("q_8X3F", { title: r.title, answer: r.answer });

Storage rolls — revision 51 evicts revision 1.

Translations

Once you’ve configured target languages (see Translations), translate a question with AI then edit before publish:

await faq.translations.questionAi("how-do-i-cancel", {
  languages: ["es", "de", "fr"]
});

// All three are now saved as status: 'draft' for your review.

await faq.translations.questionUpdate("how-do-i-cancel", "es", {
  answer: "Cleaner Spanish answer."
});

await faq.translations.questionUpdate("how-do-i-cancel", "es", {
  status: "published"
});

Source language stays authoritative — if a reader’s preferred language isn’t published yet, they see the source.