AI Document Designer with Fabric.js
Build a browser-based design tool powered by Claude AI and Fabric.js canvas.
Introduction
30 Second Summary
Design tools like Canva have made it easy for anyone to create professional documents. Now imagine combining that kind of editor with AI that generates the entire design from a simple description.
In this project, you will build a browser-based document designer where you can drag, resize, and edit shapes and text on a canvas. You will also wire up Claude AI to generate complete document layouts from natural language prompts.
What You'll Build
A full-stack web application combining an interactive Fabric.js canvas editor with a Claude-powered backend that turns text prompts into editable document designs.
By the end of this project, you'll have:
- A Vite-powered frontend with an interactive Fabric.js canvas for adding, moving, resizing, and editing shapes and text.
- An Express backend server that sends prompts to the Claude API and returns structured design data.
- A working AI generation feature that turns prompts like "Create a birthday party invitation" into editable canvas elements you can move, resize, and customize.
- Secret Mission: A template gallery where users pick from pre-made document types and AI fills them with content.
Are there any prerequisites?
You should have basic JavaScript knowledge and be comfortable running terminal commands. No prior experience with Fabric.js or the Claude API is needed.
Set Up Your Development Environment
Every web application needs a foundation before you start building features. For this project, that foundation is Node.js (the JavaScript runtime that powers both your frontend tooling and backend server).
In this step, you'll verify Node.js is ready, scaffold a project with Vite, and install the libraries that power your canvas editor and AI integration.
In this step, get ready to:
- Verify that Node.js is installed and running version 18 or higher.
- Scaffold the frontend project with Vite.
- Install Fabric.js for the canvas and set up the backend server folder.
Verify Node.js is installed
Node.js is the JavaScript runtime that runs your development server and backend API. This project requires version 18 or higher.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check your Node.js version by running this command:
node --version
✔️ I see version 18 or higher
Your terminal shows something like v18.x.x or v20.x.x or higher. Node.js is ready to go.
ⓧ I see a version lower than 18
You have Node.js installed, but this project needs version 18 or higher for compatibility with Fabric.js and the Anthropic SDK.
- Visit nodejs.org and download the latest LTS version.
- Run the installer and follow the prompts to upgrade.
- Close and reopen your terminal, then run node --version again to confirm the update.
ⓧ Command not found
Node.js is not installed on your machine yet.
- Visit nodejs.org and download the latest LTS version (20 or higher).
- Run the downloaded installer and follow the prompts to complete installation.
- Close and reopen your terminal, then run node --version again to confirm it installed successfully.
Scaffold your Vite project
Vite is a modern build tool that gives you a fast development server with instant hot reload. You'll use it to scaffold the frontend of your document designer.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold a new Vite project by running this command:
npm create vite@latest ai-doc-designer -- --template vanilla
What does this command do?
This command uses npm create to run the official Vite scaffolding tool. The --template vanilla flag tells Vite to set up a plain JavaScript project (no framework like React or Vue).
The project name ai-doc-designer becomes your folder name on the Desktop.
First time using Vite?
If this is your first time running npm create vite, your terminal may ask Need to install the following packages: create-vite@latest. Ok to proceed? Type y and press Enter to continue.
- Confirm the folder was created by running:
ls
You should see ai-doc-designer listed in the output.
- Move into your new project folder and install the base dependencies by running these commands:
cd ai-doc-designer
npm install
You should see output confirming packages were added, ending with a summary like added X packages.
Seeing an error during npm install?
Make sure you are inside the ai-doc-designer folder. Run pwd to check your current location. The path should end with ai-doc-designer.
If you see permission errors, try running the command again. help me fix npm install errors in my Vite project
Install project dependencies
Your project needs two sets of dependencies. The frontend needs Fabric.js for the interactive canvas, and the backend needs Express and the Anthropic SDK to communicate with Claude.
- Install Fabric.js for the frontend canvas by running this command:
npm install fabric@6.6.7
You should see output confirming the package was added.
Why pin a specific version?
Fabric.js recently released version 7 with breaking changes. Pinning to 6.6.7 ensures the import syntax and APIs in this project work exactly as written.
Install failed?
Double-check the version number is exactly 6.6.7. A typo in the version will cause npm to fail. Also confirm you are still inside the ai-doc-designer folder (not inside server or your Desktop).
help me troubleshoot npm install fabric@6.6.7 failing
- Create the backend server folder by running this command:
mkdir server
- Confirm the folder exists by running:
ls
You should see server listed alongside your other project files.
Now you'll open VS Code to create the server's configuration file.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to Desktop > ai-doc-designer, then click Open.
Trust the authors?
VS Code may ask if you trust the authors of this folder. Click Yes, I trust the authors to proceed.
- In the VS Code Explorer sidebar on the left, expand the server folder.
- Right-click the server folder and select New File.
- Name the file package.json and press Enter.
- Paste the following code into the new file:
{
"name": "ai-doc-designer-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.96.0",
"cors": "^2.8.5",
"express": "^4.21.0"
}
}
What does this file do?
This package.json tells Node.js about your backend server. The "type": "module" setting lets you use modern import syntax instead of require().
The three dependencies are: express (web server framework), @anthropic-ai/sdk (the official Claude API client), and cors (lets your frontend talk to the backend across different ports).
- Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Open VS Code's integrated terminal by pressing Ctrl+` (backtick key, same on both macOS and Windows).
- Navigate into the server folder and install the backend dependencies by running these commands:
cd server
npm install
You should see output confirming packages were added, ending with a line like added 64 packages (the exact number may vary).
npm install failed in the server folder?
Make sure server/package.json is saved and contains valid JSON. A missing comma or extra trailing comma will cause a parse error.
Run cat package.json in the terminal to print the file contents and look for typos.
help me fix npm install errors in my server folder
- Navigate back to the project root so you are ready for the next step:
cd ..
✔️ Awesome, I've got everything!
Great. Double check you've saved server/package.json and that npm install completed successfully in both the root folder and the server folder.
ⓧ I'd like to double check the full code
Here is the complete server/package.json file. Make sure yours matches exactly:
{
"name": "ai-doc-designer-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.96.0",
"cors": "^2.8.5",
"express": "^4.21.0"
}
}
Your development environment is ready. Next up, you'll build the interactive canvas editor where users can add shapes, text, and drag elements around the page.
Build the Canvas Editor
Your development environment is set up with Node.js, Vite, and all your dependencies installed. Now you'll build the visual canvas editor where all the design magic happens.
Fabric.js gives you a canvas with built-in selection handles, resizing, and rotation out of the box. Without it, you'd need hundreds of lines of code just to make objects draggable on an HTML5 canvas.
In this step, get ready to:
- Create the HTML structure with a toolbar, canvas, and AI panel.
- Initialize Fabric.js and wire up interactive toolbar buttons.
- Style the application with CSS for a clean editor layout.
Create the HTML structure
Your editor needs three main areas: a toolbar along the top for shape and text buttons, a central canvas where designs live, and a sidebar panel for AI generation. The Vite scaffold created a default index.html that you'll replace with this layout.
- Open index.html in the Explorer sidebar of VS Code (it's at the root of your ai-doc-designer folder).
- Select all the existing content with Cmd+A (macOS) or Ctrl+A (Windows) and replace it with the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Document Designer</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<!-- Toolbar with shape, text, and utility buttons -->
<header class="toolbar">
<button id="add-rect">Rectangle</button>
<button id="add-circle">Circle</button>
<button id="add-text">Text</button>
<input type="color" id="color-picker" value="#4f46e5" title="Pick a color" />
<button id="delete-selected">Delete</button>
<button id="clear-canvas">Clear All</button>
<button id="export-png">Download PNG</button>
</header>
<main class="workspace">
<!-- The Fabric.js canvas lives here -->
<div class="canvas-container">
<canvas id="design-canvas" width="800" height="600"></canvas>
</div>
<!-- AI generation panel on the right -->
<aside class="ai-panel">
<h2>AI Generate</h2>
<p>Describe the document you want to create:</p>
<textarea id="ai-prompt" rows="4" placeholder="e.g. Create a birthday party invitation for Sarah's 30th birthday"></textarea>
<button id="generate-btn">Generate with AI</button>
<div id="ai-status" class="status"></div>
</aside>
</main>
</div>
<script type="module" src="main.js"></script>
</body>
</html>
What does this HTML do?
The page is divided into two major sections:
- The <header class="toolbar"> holds buttons for adding shapes, picking colors, deleting, clearing, and exporting.
- The <main class="workspace"> splits the remaining space between the canvas (where Fabric.js renders) and the AI panel (where the user types prompts).
- The <script type="module"> tag loads main.js as an ES module, which lets you use import statements.
- Save index.html.
Seeing the old Vite template page?
Make sure you replaced ALL the content in index.html. If any leftover code from the Vite scaffold remains, the page won't render correctly.
help me fix my index.html file
Initialize Fabric.js and wire up toolbar buttons
With the HTML in place, you need JavaScript to bring the canvas to life. Fabric.js turns a plain <canvas> element into an interactive surface where objects can be selected, moved, resized, and rotated.
- Open main.js in VS Code (at the root of ai-doc-designer).
- Select all and replace the existing content with the following code to initialize the canvas:
import { Canvas, Rect, Circle, Textbox } from 'fabric';
// Initialize the Fabric.js canvas on our HTML canvas element
const canvas = new Canvas('design-canvas', {
width: 800,
height: 600,
backgroundColor: '#ffffff',
});
// Reference the color picker so toolbar buttons can use the selected color
const colorPicker = document.getElementById('color-picker');
What does this code do?
- import { Canvas, Rect, Circle, Textbox } from 'fabric' pulls in the specific Fabric.js classes you need. This is the v6 named-import pattern.
- new Canvas('design-canvas', {...}) wraps the <canvas> element (matched by its ID) and gives it selection, dragging, and resizing powers.
- colorPicker stores a reference to the color input so you can read whatever color the user picks.
- Add the following code below the colorPicker line to wire up the shape and text buttons:
// Add a rectangle to the canvas when the Rectangle button is clicked
document.getElementById('add-rect').addEventListener('click', () => {
const rect = new Rect({
left: 100,
top: 100,
width: 150,
height: 100,
fill: colorPicker.value,
strokeWidth: 0,
});
canvas.add(rect);
canvas.setActiveObject(rect);
});
// Add a circle to the canvas
document.getElementById('add-circle').addEventListener('click', () => {
const circle = new Circle({
left: 200,
top: 200,
radius: 50,
fill: colorPicker.value,
strokeWidth: 0,
});
canvas.add(circle);
canvas.setActiveObject(circle);
});
// Add an editable text box to the canvas
document.getElementById('add-text').addEventListener('click', () => {
const text = new Textbox('Double-click to edit', {
left: 150,
top: 150,
width: 200,
fontSize: 20,
fill: '#1e293b',
fontFamily: 'sans-serif',
});
canvas.add(text);
canvas.setActiveObject(text);
});
What does this code do?
Each button listener creates a new Fabric.js object and adds it to the canvas. The fill: colorPicker.value reads the current color from the toolbar's color picker at the moment you click.
canvas.setActiveObject(rect) selects the new object immediately, so the user sees resize handles right away. The Textbox class lets users double-click to edit text inline.
- Add the following code below the text button handler to wire up the utility buttons and prepare the AI panel references:
// Delete the currently selected object from the canvas
document.getElementById('delete-selected').addEventListener('click', () => {
const active = canvas.getActiveObject();
if (active) {
canvas.remove(active);
}
});
// Also allow deleting with keyboard Delete or Backspace keys
document.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
const active = canvas.getActiveObject();
// Only delete if the user isn't currently editing text
if (active && !active.isEditing) {
canvas.remove(active);
}
}
});
// Clear all objects from the canvas
document.getElementById('clear-canvas').addEventListener('click', () => {
canvas.clear();
canvas.backgroundColor = '#ffffff';
canvas.renderAll();
});
// Export the canvas as a high-resolution PNG image
document.getElementById('export-png').addEventListener('click', () => {
const dataURL = canvas.toDataURL({ format: 'png', multiplier: 2 });
const link = document.createElement('a');
link.download = 'my-design.png';
link.href = dataURL;
link.click();
});
// References to AI panel elements (connected in a later step)
const generateBtn = document.getElementById('generate-btn');
const aiPrompt = document.getElementById('ai-prompt');
const aiStatus = document.getElementById('ai-status');
What does this code do?
- The delete handler checks active.isEditing so pressing Backspace while typing inside a text box doesn't accidentally delete it.
- canvas.clear() removes all objects. You re-set backgroundColor because clear() wipes that too.
- canvas.toDataURL() converts the canvas to a PNG. The multiplier: 2 exports at double resolution for sharper images.
- The three AI panel references (generateBtn, aiPrompt, aiStatus) are declared now and wired up once the backend is ready.
- Save main.js.
Seeing a blank page or console errors?
Open your browser's DevTools console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows). If you see an import error, make sure fabric is installed. Run npm install fabric@6.6.7 from the project root if needed.
help me debug my Fabric.js canvas errors
Style the application layout
The HTML structure is in place but everything is unstyled. You'll use CSS flexbox to arrange the toolbar at the top, the canvas centered in the middle, and the AI panel pinned to the right.
- Open style.css in VS Code (at the root of ai-doc-designer).
- Select all and replace with the following styles for the global reset, body, and toolbar:
/* Reset default margins and use border-box sizing */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f1f5f9;
color: #1e293b;
}
/* Full-height column layout for the app shell */
.app {
display: flex;
flex-direction: column;
height: 100vh;
}
/* Toolbar stretches across the top */
.toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #ffffff;
border-bottom: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.toolbar button {
padding: 8px 16px;
border: 1px solid #cbd5e1;
border-radius: 6px;
background: #ffffff;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.toolbar button:hover {
background: #f1f5f9;
}
.toolbar #color-picker {
width: 40px;
height: 36px;
border: 1px solid #cbd5e1;
border-radius: 6px;
cursor: pointer;
}
What does this CSS do?
- The .app container uses flex-direction: column and height: 100vh to fill the entire viewport with a vertical stack.
- The toolbar uses horizontal flexbox with a gap so buttons space evenly across the top.
- Add the following styles below the toolbar code to define the workspace, canvas area, and AI panel:
/* Workspace splits horizontally between canvas and AI panel */
.workspace {
display: flex;
flex: 1;
overflow: hidden;
}
/* Center the canvas in the available space */
.canvas-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.canvas-container canvas {
background: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* AI panel fixed-width on the right side */
.ai-panel {
width: 320px;
padding: 24px;
background: #ffffff;
border-left: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
gap: 12px;
}
.ai-panel h2 {
font-size: 18px;
color: #4f46e5;
}
.ai-panel p {
font-size: 14px;
color: #64748b;
}
.ai-panel textarea {
width: 100%;
padding: 12px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 14px;
resize: vertical;
font-family: inherit;
}
.ai-panel button {
padding: 12px 16px;
background: #4f46e5;
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.ai-panel button:hover {
background: #4338ca;
}
.ai-panel button:disabled {
background: #94a3b8;
cursor: not-allowed;
}
/* Status text below the Generate button */
.status {
font-size: 13px;
color: #64748b;
min-height: 20px;
}
.status.error {
color: #dc2626;
}
What does this CSS do?
- .workspace uses horizontal flex so the canvas and AI panel sit side by side.
- .canvas-container takes up all remaining space (flex: 1) and centers the canvas both vertically and horizontally.
- .ai-panel is fixed at 320px wide. The purple generate button and disabled state give the user clear feedback on what's clickable.
- Save style.css.
- Start the Vite dev server by running this in your terminal:
npm run dev
- Open the URL shown in your terminal (usually http://localhost:5173) in your browser.
- Click the Rectangle button in the toolbar. You should see a purple rectangle appear on the white canvas with selection handles around it.
- Try clicking Text, then double-click the text object to type something new.
Canvas not showing or buttons not working?
- Check that the id values in your HTML match what the JavaScript references. For example, id="design-canvas" in HTML must match 'design-canvas' in main.js.
- If the AI panel overflows or the layout looks broken, make sure your CSS file was fully replaced. Leftover Vite template styles can conflict.
- If shapes appear but you can't drag them, confirm your Fabric.js version is 6.6.7. Run npm list fabric to check.
help me fix my canvas editor layout
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files: index.html, main.js, and style.css.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Document Designer</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<header class="toolbar">
<button id="add-rect">Rectangle</button>
<button id="add-circle">Circle</button>
<button id="add-text">Text</button>
<input type="color" id="color-picker" value="#4f46e5" title="Pick a color" />
<button id="delete-selected">Delete</button>
<button id="clear-canvas">Clear All</button>
<button id="export-png">Download PNG</button>
</header>
<main class="workspace">
<div class="canvas-container">
<canvas id="design-canvas" width="800" height="600"></canvas>
</div>
<aside class="ai-panel">
<h2>AI Generate</h2>
<p>Describe the document you want to create:</p>
<textarea id="ai-prompt" rows="4" placeholder="e.g. Create a birthday party invitation for Sarah's 30th birthday"></textarea>
<button id="generate-btn">Generate with AI</button>
<div id="ai-status" class="status"></div>
</aside>
</main>
</div>
<script type="module" src="main.js"></script>
</body>
</html>
import { Canvas, Rect, Circle, Textbox } from 'fabric';
const canvas = new Canvas('design-canvas', {
width: 800,
height: 600,
backgroundColor: '#ffffff',
});
const colorPicker = document.getElementById('color-picker');
document.getElementById('add-rect').addEventListener('click', () => {
const rect = new Rect({
left: 100,
top: 100,
width: 150,
height: 100,
fill: colorPicker.value,
strokeWidth: 0,
});
canvas.add(rect);
canvas.setActiveObject(rect);
});
document.getElementById('add-circle').addEventListener('click', () => {
const circle = new Circle({
left: 200,
top: 200,
radius: 50,
fill: colorPicker.value,
strokeWidth: 0,
});
canvas.add(circle);
canvas.setActiveObject(circle);
});
document.getElementById('add-text').addEventListener('click', () => {
const text = new Textbox('Double-click to edit', {
left: 150,
top: 150,
width: 200,
fontSize: 20,
fill: '#1e293b',
fontFamily: 'sans-serif',
});
canvas.add(text);
canvas.setActiveObject(text);
});
document.getElementById('delete-selected').addEventListener('click', () => {
const active = canvas.getActiveObject();
if (active) {
canvas.remove(active);
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
const active = canvas.getActiveObject();
if (active && !active.isEditing) {
canvas.remove(active);
}
}
});
document.getElementById('clear-canvas').addEventListener('click', () => {
canvas.clear();
canvas.backgroundColor = '#ffffff';
canvas.renderAll();
});
document.getElementById('export-png').addEventListener('click', () => {
const dataURL = canvas.toDataURL({ format: 'png', multiplier: 2 });
const link = document.createElement('a');
link.download = 'my-design.png';
link.href = dataURL;
link.click();
});
const generateBtn = document.getElementById('generate-btn');
const aiPrompt = document.getElementById('ai-prompt');
const aiStatus = document.getElementById('ai-status');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f1f5f9;
color: #1e293b;
}
.app {
display: flex;
flex-direction: column;
height: 100vh;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #ffffff;
border-bottom: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.toolbar button {
padding: 8px 16px;
border: 1px solid #cbd5e1;
border-radius: 6px;
background: #ffffff;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.toolbar button:hover {
background: #f1f5f9;
}
.toolbar #color-picker {
width: 40px;
height: 36px;
border: 1px solid #cbd5e1;
border-radius: 6px;
cursor: pointer;
}
.workspace {
display: flex;
flex: 1;
overflow: hidden;
}
.canvas-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.canvas-container canvas {
background: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.ai-panel {
width: 320px;
padding: 24px;
background: #ffffff;
border-left: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
gap: 12px;
}
.ai-panel h2 {
font-size: 18px;
color: #4f46e5;
}
.ai-panel p {
font-size: 14px;
color: #64748b;
}
.ai-panel textarea {
width: 100%;
padding: 12px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 14px;
resize: vertical;
font-family: inherit;
}
.ai-panel button {
padding: 12px 16px;
background: #4f46e5;
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.ai-panel button:hover {
background: #4338ca;
}
.ai-panel button:disabled {
background: #94a3b8;
cursor: not-allowed;
}
.status {
font-size: 13px;
color: #64748b;
min-height: 20px;
}
.status.error {
color: #dc2626;
}
Your canvas editor is fully functional. You can add shapes, type text, drag elements around, and even export your design as an image. Next up, you'll build the backend server that connects to Claude AI and turns natural language prompts into document content.
Build the Backend API Server
Your canvas editor is fully functional. You can add shapes, text, change colors, and even export designs as PNG files.
Now you need a backend server that talks to the Claude API on your behalf. The frontend will send a user's prompt to this server, which forwards it to Claude and returns structured design data that your canvas can render.
In this step, get ready to:
- Create an Express server with middleware and the Anthropic API client.
- Write a system prompt that instructs Claude to return structured JSON elements.
- Set your API key, start the server, and test the endpoint.
Set up the Express server
The Anthropic SDK is server-side only. Your API key must never be exposed in browser code. An Express server acts as a thin proxy between your frontend and the Claude API.
Why a backend proxy?
The Anthropic SDK requires Node.js 18+ and cannot run in a browser. A backend server keeps your API key secure while giving the frontend a simple endpoint to call.
- Create a new file called index.js inside the server folder.
- Add the imports and Express app setup by pasting this code:
import express from 'express';
import Anthropic from '@anthropic-ai/sdk';
import cors from 'cors';
// Create the Express app and enable middleware
const app = express();
app.use(cors());
app.use(express.json());
// Initialize the Anthropic client with API key from environment
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
What does this code do?
The three imports bring in Express (web server), the Anthropic SDK (Claude API client), and CORS (allows your frontend on a different port to call this server).
The cors() middleware lets your Vite dev server (running on a different port) make requests to this server. The express.json() middleware parses incoming JSON request bodies. The Anthropic client reads your API key from an environment variable so it stays out of your code.
Write the system prompt and API endpoint
The system prompt is the key to getting useful output from Claude. It instructs the model to return only valid JSON describing canvas elements, with specific rules about layout and design.
- Below the Anthropic client initialization, add the SYSTEM_PROMPT constant:
const SYSTEM_PROMPT = `You are a document design assistant. When the user describes a document they want to create, you respond ONLY with a JSON object containing an "elements" array. Each element is either a rectangle or text element that will be placed on an 800x600 pixel canvas.
Element types:
- Rectangle: { "type": "rectangle", "left": number, "top": number, "width": number, "height": number, "fill": "#hexcolor" }
- Text: { "type": "text", "content": "string", "left": number, "top": number, "width": number, "fontSize": number, "fill": "#hexcolor", "fontWeight": "normal" or "bold", "textAlign": "left" or "center" or "right" }
Design rules:
- Use a visually appealing color palette
- Include a background rectangle as the first element if appropriate
- Use hierarchy: large bold text for headings, smaller text for details
- Keep all elements within the 800x600 canvas bounds
- Respond with ONLY valid JSON, no markdown, no explanation`;
What does the system prompt do?
This prompt constrains Claude's output to a strict JSON schema. It defines exactly two element types (rectangle and text) with their properties, matching what your Fabric.js canvas can render.
The design rules guide Claude to produce visually appealing layouts. The instruction to respond with "ONLY valid JSON, no markdown, no explanation" prevents Claude from wrapping the output in code fences or adding commentary that would break JSON parsing.
- Below the SYSTEM_PROMPT, add the POST endpoint and server listener:
app.post('/api/generate', async (req, res) => {
const { prompt } = req.body;
if (!prompt) {
return res.status(400).json({ error: 'Prompt is required' });
}
try {
const message = await anthropic.messages.create({
model: 'claude-haiku-4-5',
max_tokens: 1024,
system: SYSTEM_PROMPT,
messages: [{ role: 'user', content: prompt }],
});
const text = message.content[0].text;
const parsed = JSON.parse(text);
res.json(parsed);
} catch (error) {
console.error('Generation error:', error.message);
res.status(500).json({ error: 'Failed to generate design' });
}
});
const PORT = 3001;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
What does this endpoint do?
The /api/generate endpoint extracts the user's prompt from the request body and validates it. Then it calls Claude's Messages API using the claude-haiku-4-5 model, which is fast and inexpensive for structured output.
Claude's response text is parsed as JSON and sent back to the frontend. If anything goes wrong (invalid API key, Claude returns malformed JSON, network error), the catch block logs the error and returns a 500 status.
- Save server/index.js.
- Confirm the file exists by running this from the server folder:
ls
- You should see index.js, package.json, and node_modules in the output.
Don't see index.js?
Make sure you created the file inside the server folder, not in the project root. In VS Code, right-click the server folder in the Explorer sidebar and select New File, then name it index.js.
help me create index.js in the right location
✔️ Awesome, I've got everything!
Great. Double check you have saved the file before continuing.
ⓧ I'd like to double check the full code
Here is the complete server/index.js file:
import express from 'express';
import Anthropic from '@anthropic-ai/sdk';
import cors from 'cors';
const app = express();
app.use(cors());
app.use(express.json());
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const SYSTEM_PROMPT = `You are a document design assistant. When the user describes a document they want to create, you respond ONLY with a JSON object containing an "elements" array. Each element is either a rectangle or text element that will be placed on an 800x600 pixel canvas.
Element types:
- Rectangle: { "type": "rectangle", "left": number, "top": number, "width": number, "height": number, "fill": "#hexcolor" }
- Text: { "type": "text", "content": "string", "left": number, "top": number, "width": number, "fontSize": number, "fill": "#hexcolor", "fontWeight": "normal" or "bold", "textAlign": "left" or "center" or "right" }
Design rules:
- Use a visually appealing color palette
- Include a background rectangle as the first element if appropriate
- Use hierarchy: large bold text for headings, smaller text for details
- Keep all elements within the 800x600 canvas bounds
- Respond with ONLY valid JSON, no markdown, no explanation`;
app.post('/api/generate', async (req, res) => {
const { prompt } = req.body;
if (!prompt) {
return res.status(400).json({ error: 'Prompt is required' });
}
try {
const message = await anthropic.messages.create({
model: 'claude-haiku-4-5',
max_tokens: 1024,
system: SYSTEM_PROMPT,
messages: [{ role: 'user', content: prompt }],
});
const text = message.content[0].text;
const parsed = JSON.parse(text);
res.json(parsed);
} catch (error) {
console.error('Generation error:', error.message);
res.status(500).json({ error: 'Failed to generate design' });
}
});
const PORT = 3001;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Start and test the server
Before starting the server, you need an API key from Anthropic. The server reads this key from an environment variable so it never appears in your code.
- Open console.anthropic.com in your browser. Create an account if you do not have one.
- Navigate to API Keys in your Account Settings.
- Click Create Key, give it a name, and copy the key value.
Keep your API key safe
Your API key is shown only once. Copy it immediately and store it somewhere safe. Do not commit it to version control or share it publicly.
- Open a new terminal in VS Code (keep your Vite dev server running in the existing terminal).
- Navigate to the server folder and set your API key as an environment variable by running:
cd server
export ANTHROPIC_API_KEY='your-api-key-here'
- Replace your-api-key-here with the key you copied from the Anthropic Console.
- Start the server by running:
npm start
- You should see Server running on http://localhost:3001 in your terminal.
Seeing an error on startup?
If you see a syntax error, check that server/package.json contains "type": "module". This enables ES module imports (the import syntax).
If you see a port conflict, another process is using port 3001. Close it or change the PORT value in server/index.js.
help me debug my Express server startup error
Now test the endpoint to confirm Claude returns valid JSON.
- Open a third terminal and send a test request by running:
curl -X POST http://localhost:3001/api/generate \
-H "Content-Type: application/json" \
-d '{"prompt": "Create a simple hello world card with a blue background"}'
- You should see a JSON response containing an elements array with rectangle and text objects. The exact content varies because Claude generates it dynamically.
What should I see?
A successful response looks something like this (your colors and positions will differ):
{
"elements": [
{ "type": "rectangle", "left": 0, "top": 0, "width": 800, "height": 600, "fill": "#1e40af" },
{ "type": "text", "content": "Hello World", "left": 200, "top": 250, "width": 400, "fontSize": 48, "fill": "#ffffff", "fontWeight": "bold", "textAlign": "center" }
]
}
The key thing is that you receive valid JSON with an elements array. Each element has a type of either rectangle or text with positioning properties.
Getting an authentication error?
If you see a 500 error or an authentication message, your ANTHROPIC_API_KEY environment variable may not be set in the terminal where the server is running. Stop the server with Ctrl+C, re-run the export command, and start the server again.
If you are on Windows using Command Prompt, use set ANTHROPIC_API_KEY=your-api-key-here instead of export.
help me fix my Claude API authentication error
Your backend is live and returning structured design data from Claude. Next, you will connect your frontend AI panel to this endpoint so clicking Generate with AI renders elements directly onto the canvas.
Connect the Frontend to the AI Backend
Your backend is running and ready to accept prompts. The missing piece is the connection between the AI panel in your browser and the Express server you just built.
In this step, you will wire the Generate with AI button to send the user's prompt to your backend, receive the structured JSON response from Claude, and render the returned elements as editable objects on the canvas.
In this step, get ready to:
- Send the user's prompt from the frontend to the backend API.
- Parse Claude's JSON response and create Fabric.js objects on the canvas.
- Add loading feedback and error handling for a smooth user experience.
Add the generate button click handler
The AI panel already has a textarea and button in your HTML. Your main.js file already grabs references to these elements. Now you need to respond when the user clicks the button.
- In main.js, find the line that says const aiStatus = document.getElementById('ai-status'); at the bottom of the file.
- Add the following click handler below it:
generateBtn.addEventListener('click', async () => {
const prompt = aiPrompt.value.trim();
if (!prompt) {
aiStatus.textContent = 'Please enter a prompt.';
aiStatus.className = 'status error';
return;
}
// Disable the button and show loading state
generateBtn.disabled = true;
aiStatus.textContent = 'Generating your design...';
aiStatus.className = 'status';
try {
// Send the prompt to our Express backend
const response = await fetch('http://localhost:3001/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
if (!response.ok) {
throw new Error('Generation failed. Check your API key and server.');
}
// Parse the response and render elements on the canvas
const data = await response.json();
renderAIElements(data.elements);
aiStatus.textContent = 'Design generated! Edit any element by clicking on it.';
aiStatus.className = 'status';
} catch (error) {
aiStatus.textContent = error.message;
aiStatus.className = 'status error';
} finally {
generateBtn.disabled = false;
}
});
What does this code do?
This handler runs when the user clicks Generate with AI. It manages the full request lifecycle.
- It first validates that the textarea is not empty.
- generateBtn.disabled = true prevents double-clicks while Claude is thinking.
- The fetch call sends the prompt to your Express server at http://localhost:3001/api/generate.
- The finally block re-enables the button whether the request succeeds or fails.
Don't save yet. The handler calls renderAIElements which doesn't exist yet. You will add it next.
Build the render function for AI-generated elements
Claude returns a JSON array of element objects. Each object describes either a rectangle or a text block with position, size, and styling. Your render function translates these descriptions into real Fabric.js objects on the canvas.
- In main.js, add the following function below the click handler you just wrote:
function renderAIElements(elements) {
// Clear existing canvas content
canvas.clear();
canvas.backgroundColor = '#ffffff';
elements.forEach((el) => {
if (el.type === 'rectangle') {
const rect = new Rect({
left: el.left,
top: el.top,
width: el.width,
height: el.height,
fill: el.fill || '#4f46e5',
strokeWidth: 0,
});
canvas.add(rect);
} else if (el.type === 'text') {
// Create editable text from AI-generated content
const text = new Textbox(el.content, {
left: el.left,
top: el.top,
width: el.width || 300,
fontSize: el.fontSize || 20,
fill: el.fill || '#1e293b',
fontFamily: 'sans-serif',
fontWeight: el.fontWeight || 'normal',
textAlign: el.textAlign || 'left',
});
canvas.add(text);
}
});
// Re-render the canvas with all new elements
canvas.renderAll();
}
What does this code do?
- canvas.clear() removes any existing elements so you start with a blank slate for each generation.
- The function loops through each element Claude returned and creates the matching Fabric.js object (Rect or Textbox).
- Default values (like el.fill || '#4f46e5') ensure elements still render even if Claude omits optional properties.
- Every element is a real Fabric.js object, so the user can drag, resize, and edit it after generation.
- Save main.js.
Seeing a red error in the status area?
- Make sure your Express server is still running in its terminal window. If it stopped, restart it from the server folder.
- Check that your ANTHROPIC_API_KEY environment variable is still set in the terminal where the server runs.
- Confirm the Vite dev server is running so your browser is serving the latest main.js.
Why is my AI generation failing?
Test the full end-to-end flow
Both servers should still be running from the previous step. Time to test the complete flow from prompt to rendered design.
- Switch to your browser tab with the Vite dev server (usually http://localhost:5173).
- In the AI panel on the right, type a prompt into the textarea. Try something like Create a birthday party invitation for Sarah's 30th birthday.
- Click Generate with AI.
You should see the button turn grey and the status text change to "Generating your design..." for a few seconds. Then colourful shapes and text appear on your canvas.
- Click on any element that appeared. You should see selection handles (resize, rotate) around it.
- Double-click a text element to edit its content directly on the canvas.
Why is every element editable?
The renderAIElements function creates standard Fabric.js objects. They automatically get the same drag, resize, and rotate controls as any element you add manually from the toolbar.
This means AI generation is a starting point. The user can always fine-tune the result by hand.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.js.
ⓧ I'd like to double check the full code
Here is the complete main.js file as it should look at the end of this step:
import { Canvas, Rect, Circle, Textbox } from 'fabric';
const canvas = new Canvas('design-canvas', {
width: 800,
height: 600,
backgroundColor: '#ffffff',
});
const colorPicker = document.getElementById('color-picker');
document.getElementById('add-rect').addEventListener('click', () => {
const rect = new Rect({
left: 100,
top: 100,
width: 150,
height: 100,
fill: colorPicker.value,
strokeWidth: 0,
});
canvas.add(rect);
canvas.setActiveObject(rect);
});
document.getElementById('add-circle').addEventListener('click', () => {
const circle = new Circle({
left: 200,
top: 200,
radius: 50,
fill: colorPicker.value,
strokeWidth: 0,
});
canvas.add(circle);
canvas.setActiveObject(circle);
});
document.getElementById('add-text').addEventListener('click', () => {
const text = new Textbox('Double-click to edit', {
left: 150,
top: 150,
width: 200,
fontSize: 20,
fill: '#1e293b',
fontFamily: 'sans-serif',
});
canvas.add(text);
canvas.setActiveObject(text);
});
document.getElementById('delete-selected').addEventListener('click', () => {
const active = canvas.getActiveObject();
if (active) {
canvas.remove(active);
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
const active = canvas.getActiveObject();
if (active && !active.isEditing) {
canvas.remove(active);
}
}
});
document.getElementById('clear-canvas').addEventListener('click', () => {
canvas.clear();
canvas.backgroundColor = '#ffffff';
canvas.renderAll();
});
document.getElementById('export-png').addEventListener('click', () => {
const dataURL = canvas.toDataURL({ format: 'png', multiplier: 2 });
const link = document.createElement('a');
link.download = 'my-design.png';
link.href = dataURL;
link.click();
});
const generateBtn = document.getElementById('generate-btn');
const aiPrompt = document.getElementById('ai-prompt');
const aiStatus = document.getElementById('ai-status');
generateBtn.addEventListener('click', async () => {
const prompt = aiPrompt.value.trim();
if (!prompt) {
aiStatus.textContent = 'Please enter a prompt.';
aiStatus.className = 'status error';
return;
}
generateBtn.disabled = true;
aiStatus.textContent = 'Generating your design...';
aiStatus.className = 'status';
try {
const response = await fetch('http://localhost:3001/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
if (!response.ok) {
throw new Error('Generation failed. Check your API key and server.');
}
const data = await response.json();
renderAIElements(data.elements);
aiStatus.textContent = 'Design generated! Edit any element by clicking on it.';
aiStatus.className = 'status';
} catch (error) {
aiStatus.textContent = error.message;
aiStatus.className = 'status error';
} finally {
generateBtn.disabled = false;
}
});
function renderAIElements(elements) {
canvas.clear();
canvas.backgroundColor = '#ffffff';
elements.forEach((el) => {
if (el.type === 'rectangle') {
const rect = new Rect({
left: el.left,
top: el.top,
width: el.width,
height: el.height,
fill: el.fill || '#4f46e5',
strokeWidth: 0,
});
canvas.add(rect);
} else if (el.type === 'text') {
const text = new Textbox(el.content, {
left: el.left,
top: el.top,
width: el.width || 300,
fontSize: el.fontSize || 20,
fill: el.fill || '#1e293b',
fontFamily: 'sans-serif',
fontWeight: el.fontWeight || 'normal',
textAlign: el.textAlign || 'left',
});
canvas.add(text);
}
});
canvas.renderAll();
}
Your AI document designer now works end-to-end. Type a prompt, click generate, and watch Claude design a document for you. Next up, you will add export functionality and polish the user experience with keyboard shortcuts and a colour picker.
Add Export and Polish
Your AI document designer is fully functional. You can add shapes, type text, generate AI content, and edit everything on the canvas.
In this final step, you'll test the export, cleanup, and polish features that are already wired into your code. These are the finishing touches that make the app feel complete and professional.
In this step, get ready to:
- Export a canvas design as a high-resolution PNG image.
- Test the Clear All button and Delete key for removing elements.
- Verify the color picker, disabled button states, and smooth transitions.
Test the Download PNG export
A design tool is only useful if you can save your work. Your app already has a Download PNG button in the toolbar that exports the canvas as an image file.
- In your browser, add a few elements to the canvas using the Rectangle, Circle, and Text buttons.
- Click Download PNG in the toolbar.
A file named my-design.png downloads to your computer. Open it to see your design at double resolution.
Why is the export at 2x resolution?
The export code uses multiplier: 2 in the toDataURL call. This creates an image that's 1600x1200 pixels instead of 800x600.
A 2x export looks crisp on high-DPI screens (Retina displays) and prints at higher quality. This is a common pattern in design tools.
Here's the export code already in your main.js:
document.getElementById('export-png').addEventListener('click', () => {
const dataURL = canvas.toDataURL({ format: 'png', multiplier: 2 });
const link = document.createElement('a');
link.download = 'my-design.png';
link.href = dataURL;
link.click();
});
What does this code do?
- canvas.toDataURL() converts the entire Fabric.js canvas into a base64-encoded PNG string.
- The code creates an invisible <a> link element, sets its download attribute to name the file, then triggers a click to start the download.
PNG not downloading?
- Check that your browser isn't blocking the download. Look for a blocked-download notification in the address bar.
- Make sure the canvas has at least one element on it before exporting.
My PNG export isn't working. The canvas has elements but nothing downloads when I click the button.
Test Clear All and Delete
Design tools need quick ways to remove elements. Your app supports two deletion methods: removing individual selected objects and clearing the entire canvas.
- Click on any element on your canvas to select it (you'll see the blue selection handles).
- Click the Delete button in the toolbar. The selected element disappears.
- Add another element, select it, then press the Delete or Backspace key on your keyboard. It also removes the element.
- Add several shapes and text elements to the canvas.
- Click Clear All in the toolbar. Everything is removed and the canvas resets to white.
Why check for isEditing before deleting?
The keyboard delete handler includes a check: !active.isEditing. Without this, pressing Backspace while editing text inside a Textbox would delete the entire element instead of just removing a character.
Verify the color picker and UI polish
The toolbar includes a color picker that controls the fill color of new shapes. The CSS also includes transitions and disabled states that make the app feel responsive.
- Click the color swatch in the toolbar (the small colored square between the Text button and the Delete button).
- Pick a bright color from the color picker popup.
- Click Rectangle to add a new shape. It appears in the color you just picked.
- Type a prompt into the AI panel and click Generate with AI. Notice the button grays out and becomes unclickable while generating.
How does the disabled button state work?
When the generate button is clicked, the JavaScript sets generateBtn.disabled = true. The CSS rule .ai-panel button:disabled changes the background to gray and sets cursor: not-allowed. This prevents double-clicks and gives the user clear feedback.
The transition: background 0.2s property on buttons creates smooth hover effects. Small details like these make the difference between a prototype and a polished app.
Double-click to edit text
Try double-clicking any text element on the canvas. Fabric.js Textbox elements are editable by default. You can change the text content directly on the canvas without needing a separate input field.
✔️ Awesome, I've got everything!
Great. Double check that you've saved all your files. Your AI Document Designer is now fully polished and ready to use.
ⓧ I'd like to double check the full code
Here's the full main.js for reference:
import { Canvas, Rect, Circle, Textbox } from 'fabric';
const canvas = new Canvas('design-canvas', {
width: 800,
height: 600,
backgroundColor: '#ffffff',
});
const colorPicker = document.getElementById('color-picker');
document.getElementById('add-rect').addEventListener('click', () => {
const rect = new Rect({
left: 100,
top: 100,
width: 150,
height: 100,
fill: colorPicker.value,
strokeWidth: 0,
});
canvas.add(rect);
canvas.setActiveObject(rect);
});
document.getElementById('add-circle').addEventListener('click', () => {
const circle = new Circle({
left: 200,
top: 200,
radius: 50,
fill: colorPicker.value,
strokeWidth: 0,
});
canvas.add(circle);
canvas.setActiveObject(circle);
});
document.getElementById('add-text').addEventListener('click', () => {
const text = new Textbox('Double-click to edit', {
left: 150,
top: 150,
width: 200,
fontSize: 20,
fill: '#1e293b',
fontFamily: 'sans-serif',
});
canvas.add(text);
canvas.setActiveObject(text);
});
document.getElementById('delete-selected').addEventListener('click', () => {
const active = canvas.getActiveObject();
if (active) {
canvas.remove(active);
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
const active = canvas.getActiveObject();
if (active && !active.isEditing) {
canvas.remove(active);
}
}
});
document.getElementById('clear-canvas').addEventListener('click', () => {
canvas.clear();
canvas.backgroundColor = '#ffffff';
canvas.renderAll();
});
document.getElementById('export-png').addEventListener('click', () => {
const dataURL = canvas.toDataURL({ format: 'png', multiplier: 2 });
const link = document.createElement('a');
link.download = 'my-design.png';
link.href = dataURL;
link.click();
});
const generateBtn = document.getElementById('generate-btn');
const aiPrompt = document.getElementById('ai-prompt');
const aiStatus = document.getElementById('ai-status');
generateBtn.addEventListener('click', async () => {
const prompt = aiPrompt.value.trim();
if (!prompt) {
aiStatus.textContent = 'Please enter a prompt.';
aiStatus.className = 'status error';
return;
}
generateBtn.disabled = true;
aiStatus.textContent = 'Generating your design...';
aiStatus.className = 'status';
try {
const response = await fetch('http://localhost:3001/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt }),
});
if (!response.ok) {
throw new Error('Generation failed. Check your API key and server.');
}
const data = await response.json();
renderAIElements(data.elements);
aiStatus.textContent = 'Design generated! Edit any element by clicking on it.';
aiStatus.className = 'status';
} catch (error) {
aiStatus.textContent = error.message;
aiStatus.className = 'status error';
} finally {
generateBtn.disabled = false;
}
});
function renderAIElements(elements) {
canvas.clear();
canvas.backgroundColor = '#ffffff';
elements.forEach((el) => {
if (el.type === 'rectangle') {
const rect = new Rect({
left: el.left,
top: el.top,
width: el.width,
height: el.height,
fill: el.fill || '#4f46e5',
strokeWidth: 0,
});
canvas.add(rect);
} else if (el.type === 'text') {
const text = new Textbox(el.content, {
left: el.left,
top: el.top,
width: el.width || 300,
fontSize: el.fontSize || 20,
fill: el.fill || '#1e293b',
fontFamily: 'sans-serif',
fontWeight: el.fontWeight || 'normal',
textAlign: el.textAlign || 'left',
});
canvas.add(text);
}
});
canvas.renderAll();
}
And the full style.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f1f5f9;
color: #1e293b;
}
.app {
display: flex;
flex-direction: column;
height: 100vh;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #ffffff;
border-bottom: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.toolbar button {
padding: 8px 16px;
border: 1px solid #cbd5e1;
border-radius: 6px;
background: #ffffff;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.toolbar button:hover {
background: #f1f5f9;
}
.toolbar #color-picker {
width: 40px;
height: 36px;
border: 1px solid #cbd5e1;
border-radius: 6px;
cursor: pointer;
}
.workspace {
display: flex;
flex: 1;
overflow: hidden;
}
.canvas-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.canvas-container canvas {
background: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.ai-panel {
width: 320px;
padding: 24px;
background: #ffffff;
border-left: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
gap: 12px;
}
.ai-panel h2 {
font-size: 18px;
color: #4f46e5;
}
.ai-panel p {
font-size: 14px;
color: #64748b;
}
.ai-panel textarea {
width: 100%;
padding: 12px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 14px;
resize: vertical;
font-family: inherit;
}
.ai-panel button {
padding: 12px 16px;
background: #4f46e5;
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.ai-panel button:hover {
background: #4338ca;
}
.ai-panel button:disabled {
background: #94a3b8;
cursor: not-allowed;
}
.status {
font-size: 13px;
color: #64748b;
min-height: 20px;
}
.status.error {
color: #dc2626;
}
Your AI Document Designer is complete. You have a fully functional canvas editor with AI generation, export capabilities, and polished interactions. Next up, try extending it with a template gallery in the Secret Mission.
Secret mission
Add a Template Gallery
Your document designer works great for freeform creation. But what if users could pick a starting template and let AI fill in the content? Build a template gallery that combines pre-made layouts with Claude-generated text.
Clean Up Your Resources
Clean Up Your Resources
This project uses the Claude API, which charges per token. API costs only occur when you actively send requests to the server. There is no persistent infrastructure running in the background.
Decide whether to keep your resources as-is, or delete them entirely.
Resources you used:
- ai-doc-designer project folder on your Desktop (frontend code, node_modules).
- server folder inside the project (Express backend, node_modules).
- Anthropic API key (used to authenticate with the Claude API).
Keep everything running
No action needed. Choose this if you plan to keep building on this project or experimenting with new prompts.
- Your project files stay on your Desktop.
- Your API key remains active, but you are only charged when you make requests.
- At Claude Haiku 4.5 rates ($1/MTok input, $5/MTok output), a typical generation costs well under $0.01.
Pause - I'll come back to this later
There is no persistent infrastructure to shut down. Your local files and API key simply wait for you.
- Stop the Vite dev server and Express server by pressing Ctrl+C in each terminal window.
- No ongoing costs accrue while the servers are stopped.
Delete - I don't want to use this again
Remove all project files and revoke your API key so nothing remains.
- Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/ai-doc-designer
- On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\ai-doc-designer
- Remove the ANTHROPIC_API_KEY environment variable from your shell profile (~/.zshrc or ~/.bashrc). Open the file, delete the export ANTHROPIC_API_KEY=... line, and save.
- Delete your API key from the Anthropic Console. Go to console.anthropic.com, open Settings, then API Keys. Find the key you created for this project and delete it.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional AI-powered document designer from scratch.
Here's what you learned:
- Built an interactive canvas editor with Fabric.js supporting drag, resize, rotate, and text editing.
- Created an Express backend that communicates with the Claude API to generate structured content.
- Implemented AI-powered document generation that turns natural language prompts into visual canvas elements.
- Added export functionality to download designs as PNG images.
- Secret Mission: Built a template gallery that combines pre-made layouts with AI content generation.
Ready to quiz yourself?