Forms with Sanity
Common approaches for using forms with Sanity
Go to Forms with SanityThis guide will explain the 2 common patterns for integrating external data sources with Sanity. Our plugins page includes pre-built integrations for popular platforms, but if an integration doesn’t exist this guide can help walk you through how to build one of your own.
There are 2 primary ways to bring external data into Sanity:
Creating a document for each item in an external system is often referred to as a sync plugin in Sanity terminology. The best example of a Sanity sync plugin would be Sanity Connect for Shopify, which has excellent documentation showing custom handlers that illustrate the approximate process a sync plugin uses to keep Sanity up to date with external data:
Pros
Cons
The following is a simplified breakdown of the code example shown in the Shopify Sanity Connect docs. It shows a serverless function that receives a webhook from Shopify when products are created, updated, or deleted, and syncs those changes to documents in Sanity.
// ./src/pages/api/sync-handler.ts
import { createClient } from "@sanity/client";
// Create a Sanity client with a write token to allow creating and updating of documents
// Read more on auth, tokens and securing them: https://www.sanity.io/docs/http-auth
const sanityClient = createClient({
apiVersion: "2025-02-04",
dataset: process.env.SANITY_DATASET,
projectId: process.env.SANITY_PROJECT_ID,
token: process.env.SANITY_ADMIN_AUTH_TOKEN,
useCdn: false,
});
/**
* A Next.js API route handler for the pages router
* Takes incoming webhooks and creates/updates/deletes documents based on external system's changes
*/
export default async function handler(req, res) {
const { body } = req;
try {
// Create a transaction to batch operations to Sanity
const transaction = sanityClient.transaction();
// Perform different operations based on the webhook action type
switch (body.action) {
case "create":
case "update":
case "sync":
await createOrUpdateProducts(transaction, body.products);
break;
case "delete":
const documentIds = body.productIds.map((id) =>
getDocumentProductId(id)
);
await deleteProducts(transaction, documentIds);
break;
}
await transaction.commit();
} catch (err) {
console.error("Transaction failed: ", err.message);
}
res.status(200).json({ message: "OK" });
}
/**
* Creates (or updates if already existing) Sanity documents of type `shopify.product`.
* Patches existing drafts too, if present.
*
* All products will be created with a deterministic _id in the format `product-${SHOPIFY_ID}`
*/
async function createOrUpdateProducts(transaction, products) {
// Extract draft document IDs from current update
const draftDocumentIds = products.map((product) => {
const productId = extractIdFromGid(product.id);
return `drafts.${getDocumentProductId(productId)}`;
});
// Determine if drafts exist for any updated products
const existingDrafts = await sanityClient.fetch(`*[_id in $ids]._id`, {
ids: draftDocumentIds,
});
products.forEach((product) => {
// Build Sanity product document
const document = buildProductDocument(product);
const draftId = `drafts.${document._id}`;
// Create (or update) existing published document
transaction
.createIfNotExists(document)
.patch(document._id, (patch) => patch.set(document));
// Check if this product has a corresponding draft and if so, update that too.
if (existingDrafts.includes(draftId)) {
transaction.patch(draftId, (patch) =>
patch.set({
...document,
_id: draftId,
})
);
}
});
}
/**
* Delete corresponding Sanity documents of type `shopify.product`.
* Published and draft documents will be deleted.
*/
async function deleteProducts(transaction, documentIds) {
documentIds.forEach((id) => {
transaction.delete(id).delete(`drafts.${id}`);
});
}
/**
* Build Sanity document from webhook product payload
*/
function buildProductDocument(product) {
const {
featuredImage,
id,
productType,
priceRange,
status,
title,
productId
} = product;
// Build Sanity document
return {
_id: getDocumentProductId(productId),
_type: "shopify.product",
image: featuredImage?.src,
priceRange,
productType,
status,
title,
};
}
/**
* Map Shopify product ID number to a corresponding Sanity document ID string
* e.g. 12345 => product-12345
*/
function getDocumentProductId(productId) {
return `product-${productId}`;
}
Again, this is simplified to illustrate a typical workflow, check out the example in the Sanity Connect docs for a better real world example, including things like better error handling that were removed here for brevity.
Integrating external data as field values is typically referred to as an input plugin. Input plugins will provide a custom field type that includes an input for browsing the data in the external system from the Studio, where selecting an item sets the field’s value. The data saved to the field can vary in complexity to match your use case, some plugins will save just a string for an item’s name or ID, others will copy an object with several properties, like a set of URLs for different image formats.
@sanity/sanity-plugin-async-list provides an easy starting point for this type of implementation
Pros
Cons
id
.Here’s an example using @sanity/sanity-plugin-async-list to fetch the names of Disney Characters and add them as an input in a document.
// sanity.config.ts
import {defineConfig} from 'sanity'
import {asyncList} from '@sanity/sanity-plugin-async-list'
export default defineConfig({
// ...rest of config
plugins: [
asyncList({
schemaType: 'disneyCharacter',
loader: async () => {
const response = await fetch('https://api.disneyapi.dev/character')
const result: {data: {name: string}[]} = await response.json()
return result.data.map((item) => {
return {value: item.name, ...item}
})
},
}),
// ...rest of plugins
],
})
Add the name from schemaType to the document type where you want to use the field
// post.ts
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'post',
title: 'Post',
type: 'document',
fields: [
defineField({
name: 'disney',
type: 'disneyCharacter',
}),
],
})
Then see the field in your Studio
Sanity Composable Content Cloud is the headless CMS that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when needed. With the hosted document store, you query content freely and easily integrate with any framework or data source to distribute and enrich content.
Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.
Common approaches for using forms with Sanity
Go to Forms with SanityHow to use Sanity to control redirects in your JavaScript framework of choice.
Go to Managing redirects with SanityA complete guide to setting up your blog using Astro and Sanity
Go to Build your blog with Astro and Sanity