Best way to manage API-fetched data and dynamic category hierarchy in Sanity?

42 replies
Last updated: Apr 20, 2021
Hey there, in you experience with sanity what’s the best way to manage something that can be fetched from an api? Eg. Category order from an e-commerce? I’m trying to get category dynamically from another api and if it’s possibile set documents as child of a singleton document. Any tips?
AI Update

Great question! Managing external API data in Sanity is a common scenario, especially for e-commerce integrations. Let me share some approaches based on what works well in practice.

The Modern Approach: Sanity Functions

For dynamically syncing data from external APIs (like e-commerce category orders), I'd recommend using Sanity Functions as your first choice. Functions are serverless compute that run directly on Sanity's infrastructure and are perfect for this use case:

Benefits:

  • No external hosting needed - runs on Sanity's infrastructure
  • Event-driven triggers (when categories change, update your data)
  • Can run on a schedule or via webhooks from your e-commerce platform
  • Direct access to Sanity's client API for creating/updating documents
  • Node.js v22 runtime with TypeScript support

Example use case: Set up a Function that fetches categories from your e-commerce API and creates/updates corresponding documents in Sanity. You could trigger this on a schedule or when your e-commerce platform sends a webhook.

The Enterprise Option: Sanity Connect

If you're working with Shopify specifically, Sanity Connect for Shopify provides turnkey bi-directional sync. It automatically syncs products, collections, and categories from Shopify into Sanity documents. This is the most robust option for Shopify integrations, handling all the sync logic for you.

Modeling Parent-Child Relationships

For your specific question about setting documents as children of a singleton document, here's the pattern:

Option 1: Singleton with Reference Array (Recommended for your use case)

Create a singleton document that contains an array of references to your category documents:

{
  name: 'categoryOrder',
  type: 'document',
  fields: [
    {
      name: 'categories',
      type: 'array',
      of: [{ type: 'reference', to: [{ type: 'category' }] }]
    }
  ]
}

This gives you a single place to manage category order. When your Function syncs categories from the external API, it can update both the individual category documents AND this singleton's reference array to maintain order.

Option 2: Use the Orderable Document List Plugin

For manual reordering in Studio, install the @sanity/orderable-document-list plugin. It adds drag-and-drop ordering that persists via an orderRank field. However, since you're fetching order from an external API, you'd probably want to programmatically set this field in your sync function.

Practical Implementation Pattern

Here's how I'd approach this:

  1. Create category documents for each category from your API
  2. Use a singleton to maintain the canonical order (array of references)
  3. Set up a Sanity Function that:
    • Fetches categories from your e-commerce API
    • Creates/updates category documents in Sanity
    • Updates the singleton with the correct order
  4. Query efficiently - Frontend can fetch everything in one GROQ query by starting with the singleton and dereferencing
*[_type == "categoryOrder"][0] {
  categories[]-> {
    _id,
    title,
    // other fields
  }
}

Alternative: Traditional Webhooks

If Functions don't fit your needs, you can still use traditional webhooks with your own serverless functions (Vercel, Netlify, etc.). However, Functions are generally easier since they're integrated directly into your Sanity project as described in this guide on integrating external data.

The key insight is that Sanity excels at being the "source of truth" for your content operations, even when that content originates elsewhere. The sync pattern (whether via Functions or Connect) creates a unified API where your frontend can query everything from Sanity in one request.

Show original thread
42 replies
Hey User 👋
I think there are 2 parts to this:
1. getting the data from the other API before rendering items (an async structure).
This snippet may help you with this part.2. displaying the categories from that data
For 2, what is your goal there? Will documents also live in Sanity and be enhanced with data in the studio? Or do you simply want to list categories without altering them?

If you'll enhance them, your general approach seems quite sound, but you may want to re-think the
S.document().documentId("categoriesSettings")
portion. The way it's currently structured would make so every document has the same
_id
, essentially rendering the same data. Maybe you can do
.documentId("external-category" + element.toLowerCase())
to provide unique _ids in Sanity anchored in the original category?
I also think you'll probably need a
S.list( ) for rendering the categories as children of the "Gestione Categorie"
listItem()
... Hope this helps! Let me know how I can be of further service 🙂
Hey User thank you. My goal is to fetch the categories that I already have in my db (with all vehicles) and handle them in sanity (eg, ordering of products, specific texts, other stuff)
Gotcha! So you're in the right track. Here's some pseudo code that could help you out:
S.listItem()
  .title('Gestione Categorie')
  .child(async () => {
    const categories = await getCategories()
    return S.list().id("categories").items(
      categories.forEach((element) => {
        S.document()
          .title(element)
          .schemaType('categoriesSettings')
          // Unique _id for each category
          .documentId(`category-${element.toLowerCase().replace(/\s/, "-")}`);
      })
    )
  }
  ),
See how in line 3 we're using an async function as the
child
of your listItem? That's how you can always get fresh data 🙂
oh yes, clear. But the fresh data is got when the sanity studio starts? or in build time?
When the editor opens that specific listItem menu - AKA when they click "Gestione Categorie"
wow this is super amazing man
If you want to reduce the amount of querying done to your db, you can cache the data in a global variable, too, just make sure to burst it every once in a while. Some editors tend to leave Sanity open for days at a time 🙂
lol yes sure
so to be clear
this code
const categories = ['city-car', 'suv', 'mini-van'];
export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Gestione Categorie')
        .child(async () => {
          // const categories = await getCategories();
          return S.list()
            .id('categories')
            .items(
              categories.forEach((element) => {
                S.document()
                  .title(element)
                  .schemaType('categoriesSettings')
                  // Unique _id for each category
                  .documentId(
                    `category-${element.toLowerCase().replace(/\s/, '-')}`
                  );
              })
            );
        }),
      // Add a visual divider (optional)
      S.divider(),
      // List out the rest of the document types, but filter out the config type
      ...S.documentTypeListItems().filter(
        (listItem) => !['categoriesSettings'].includes(listItem.getId())
      ),
    ]);
should rendere that 3 categories right?
cause S.list renders the childern
Yes! Good for testing, right? Then you can "graduate" into the async function when you're ready for it
But nothing has been shown
I will try to dig in to it
I can try it myself in a bit, maybe I typed something wrong in my example. Let me know if you can't fix it
nope buddy
can’t find the issue 😕
i’ve tried this
return S.list()
            .id('categories')
            .items(
              ...categories.map((element) => {
                return S.document()
                  .title(element)
                  .schemaType('categoriesSettings')
                  .documentId(
                    `category-${element.toLowerCase().replace(/\s/, '-')}`
                  );
              })
            );
but i got
items
must be an array of items
yey I got the results!!!!
🎉
Now I realized we were usuing .forEach in the example above instead of map. Typos...
😅
yeah
this is my solution
import S from '@sanity/desk-tool/structure-builder';

const categories = ['city-car', 'suv', 'mini-van'];
export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Gestione Categorie')

        .child(async () => {
          // const categories = await getCategories();
          return S.list()
            .id('categories')
            .items(
              categories.map((element) => {
                return S.documentListItem()
                  .id(`category-${element.toLowerCase().replace(/\s/, '-')}`)
                  .title(element)
                  .schemaType('categoriesSettings');
              })
            );
        }),
      // Add a visual divider (optional)
      S.divider(),
      // List out the rest of the document types, but filter out the config type
      ...S.documentTypeListItems().filter(
        (listItem) => !['categoriesSettings'].includes(listItem.getId())
      ),
    ]);
i’m just figuring out how to specify the id for documentListItem
You can replace the `.id(
category-...
portion of it with the desired template you want 🙂
I got it buddy 🙂
thanks for your help really apreciated
You got it! Excited to see what you build with it, User 🙌
I will show the final result 🙂 of course!
Man
I’ve made a mistake, I should return document instead of listItem inside the first listItem
is this possibile?
something like
export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Gestione Categorie')
        .child(async () => {
          const { data: categories } = await getCategories();
          // const categories = await getCategories();
          S.list()
            .id('categories')
            .items(
              categories?.map((element) => {
                S.document()
                  .title(carTypes[element])
                  .schemaType('categoriesSettings')
                  // Unique _id for each category
                  .documentId(
                    `category-${element.toLowerCase().replace(/\s/, '-')}`
                  );
              })
            );
        }),
      // Add a visual divider (optional)
      S.divider(),
      // List out the rest of the document types, but filter out the config type
      ...S.documentTypeListItems().filter(
        (listItem) => !['categoriesSettings'].includes(listItem.getId())
      ),
    ]);
but nothing is rendered
probably i found the solution
Failed prop type: The prop `type` is marked as required in `DeskToolPane`, but its value is `undefined`.
but i can’t figure out this
export default () =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Gestione Categorie')
        .child(async () => {
          const { data: categories } = await getCategories();
          return categories?.map((element) => {
            return S.document()
              .title(carTypes[element])
              .documentId(`category-${element.toLowerCase()}`)
              .schemaType('categoriesSettings');
          });
        }),
      // Add a visual divider (optional)d
      S.divider(),
      // List out the rest of the document types, but filter out the config type
      ...S.documentTypeListItems().filter(
        (listItem) => !['categoriesSettings'].includes(listItem.getId())
      ),
    ]);

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?