Saving is engine.toDocument(). A Fabric.js load from JSON is engine.loadDocument(json). Both work without any storage, so you can keep the JSON wherever you like.
Loading the live demo. Its code is below.
"use client";
import type { FabricDocument } from "fabricjs-document-engine";
import { useDocumentEngine } from "fabricjs-document-engine/react";
import { CircleIcon, FolderOpenIcon, SaveIcon, SquareIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import {
DemoCanvas,
DemoFrame,
newShape,
seedShapes,
Status,
Toolbar,
ToolButton,
} from "./demo-ui";
import { useFabricCanvas } from "./use-fabric-canvas";
export default function SaveLoadDemo() {
const t = useTranslations("demos");
const { elementRef, canvas } = useFabricCanvas();
const engine = useDocumentEngine(canvas);
const [saved, setSaved] = useState<FabricDocument>();
useEffect(() => {
if (!engine) return;
engine.canvas.add(...seedShapes());
engine.clearHistory();
}, [engine]);
function save() {
// The whole canvas as plain JSON. Store it anywhere.
setSaved(engine?.toDocument());
}
async function load() {
if (saved) await engine?.loadDocument(saved);
}
return (
<DemoFrame>
<DemoCanvas elementRef={elementRef} label={t("canvas")} />
<Toolbar>
<ToolButton
icon={SquareIcon}
label={t("square")}
onClick={() => canvas?.add(newShape("square"))}
/>
<ToolButton
icon={CircleIcon}
label={t("circle")}
onClick={() => canvas?.add(newShape("circle"))}
/>
<span className="ml-auto flex gap-1.5">
<ToolButton
icon={SaveIcon}
label={t("saveLoad.save")}
primary
disabled={!engine}
onClick={save}
/>
<ToolButton
icon={FolderOpenIcon}
label={t("saveLoad.load")}
disabled={!saved}
onClick={() => void load()}
/>
</span>
</Toolbar>
<Status tone={saved ? "ok" : "idle"}>
{saved
? t("saveLoad.saved", { count: saved.objects.length })
: t("saveLoad.hint")}
</Status>
<pre className="h-32 overflow-auto rounded-lg border bg-muted p-3 font-mono text-xs">
{saved ? JSON.stringify(saved, null, 2) : "{}"}
</pre>
</DemoFrame>
);
}Save the canvas
const document = engine.toDocument();
await fetch(`/api/documents/${document.id}`, {
method: "PUT",
body: JSON.stringify(document),
});The result is a plain FabricDocument object. It holds the canvas size and background, every object in stacking order, the images and fonts the document needs, and your own metadata. The document format page describes each field.
Compared with Fabric.js toJSON, this serialization adds:
- a stable
idon every object, including children of groups, - the properties of your custom objects, without listing them on each call,
- a
schemaVersion, so future versions can upgrade old files.
Fabric.js load from JSON
const response = await fetch("/api/documents/plan-42");
await engine.loadDocument(await response.json());Before the canvas is touched, the engine:
- validates the document and reports the exact path of any problem,
- upgrades older documents and plain Fabric.js JSON,
- refuses unknown object types, instead of turning them into something else,
- loads every image and font, and fails if an image is missing.
Only then does it turn the JSON to canvas objects. A failed load throws a DocumentEngineError and leaves the current canvas exactly as it was. With plain loadFromJSON, the canvas may already be cleared when an image fails.
When loads overlap
If the user opens document A and then document B before A finishes, B wins. The load of A is cancelled with LOAD_ABORTED, so it can never replace B:
engine.on("load:error", ({ error }) => {
if (error.code === "LOAD_ABORTED") return;
showMessage(`The document could not be opened: ${error.message}`);
});Save and load with storage
With a storage adapter, you work by id instead of by JSON:
await engine.load("plan-42");
await engine.save();This also turns on autosave and save conflict checks. See storage adapters for the built-in options.
Unsaved work is never replaced
With a storage adapter, load, loadDocument, importFabricJson and newDocument refuse with UNSAVED_CHANGES while there are unsaved changes. Save first, or pass { discardUnsavedChanges: true } when the user chose to throw the changes away.
Open old Fabric.js JSON
A Fabric.js load from JSON also works for plain canvas.toJSON() output from Fabric.js 5, 6 or 7. It is upgraded when it is opened. See migration.
Next steps
- Custom objects: keep your own properties.
- Images and fonts: what happens when an image is missing.