Skills Development Webflow Local Dev Loop Setup

Webflow Local Dev Loop Setup

v20260423
webflow-local-dev-loop
A comprehensive guide to configuring a fast, reproducible local development workflow for Webflow Data API v2 integrations. This setup includes TypeScript, hot reloading, Vitest mocking, and ngrok webhook tunneling, enabling developers to establish a rapid iteration cycle for seamless Webflow development and testing.
Get Skill
494 downloads
Overview

Webflow Local Dev Loop

Overview

Set up a fast, reproducible local development workflow for Webflow Data API v2 integrations
with TypeScript, hot reload, vitest mocking, and ngrok webhook tunneling.

Prerequisites

  • Completed webflow-install-auth setup
  • Node.js 18+ with npm/pnpm
  • API token with required scopes
  • ngrok (optional, for webhook testing)

Instructions

Step 1: Project Structure

my-webflow-project/
├── src/
│   ├── webflow/
│   │   ├── client.ts          # WebflowClient singleton
│   │   ├── collections.ts    # CMS collection operations
│   │   ├── sites.ts           # Site operations
│   │   └── types.ts           # Shared types
│   ├── webhooks/
│   │   └── handler.ts         # Webhook endpoint
│   └── index.ts
├── tests/
│   ├── collections.test.ts
│   └── fixtures/
│       └── mock-items.json
├── .env.local                 # Local secrets (git-ignored)
├── .env.example               # Template for team
├── tsconfig.json
├── vitest.config.ts
└── package.json

Step 2: Package Configuration

{
  "name": "my-webflow-project",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "test": "vitest run",
    "test:watch": "vitest --watch",
    "tunnel": "ngrok http 3000"
  },
  "dependencies": {
    "webflow-api": "^3.3.0",
    "express": "^4.21.0"
  },
  "devDependencies": {
    "tsx": "^4.19.0",
    "typescript": "^5.6.0",
    "vitest": "^2.1.0",
    "@types/express": "^5.0.0",
    "@types/node": "^22.0.0"
  }
}

Step 3: Environment Setup

# .env.example
WEBFLOW_API_TOKEN=your-token-here
WEBFLOW_SITE_ID=your-site-id
WEBFLOW_WEBHOOK_SECRET=your-webhook-secret
NODE_ENV=development
cp .env.example .env.local
# Edit .env.local with real values

Load environment in your app:

// src/webflow/client.ts
import { WebflowClient } from "webflow-api";
import { config } from "dotenv";

config({ path: ".env.local" });

let client: WebflowClient | null = null;

export function getWebflowClient(): WebflowClient {
  if (!client) {
    const token = process.env.WEBFLOW_API_TOKEN;
    if (!token) throw new Error("WEBFLOW_API_TOKEN not set in .env.local");

    client = new WebflowClient({
      accessToken: token,
    });
  }
  return client;
}

export function getSiteId(): string {
  const siteId = process.env.WEBFLOW_SITE_ID;
  if (!siteId) throw new Error("WEBFLOW_SITE_ID not set in .env.local");
  return siteId;
}

Step 4: Hot Reload Development

# Start with hot reload — restarts on file changes
npm run dev

The tsx watch command re-executes your entry file on every save. For an Express
webhook server:

// src/index.ts
import express from "express";
import { getWebflowClient, getSiteId } from "./webflow/client.js";

const app = express();
app.use(express.json());

app.get("/health", async (req, res) => {
  const webflow = getWebflowClient();
  try {
    const { sites } = await webflow.sites.list();
    res.json({ status: "healthy", sites: sites?.length });
  } catch (error) {
    res.status(503).json({ status: "unhealthy", error: String(error) });
  }
});

app.listen(3000, () => console.log("Dev server: http://localhost:3000"));

Step 5: Testing with Vitest

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "node",
    globals: true,
    setupFiles: ["./tests/setup.ts"],
  },
});
// tests/setup.ts
import { config } from "dotenv";
config({ path: ".env.local" });
// tests/collections.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { WebflowClient } from "webflow-api";

// Mock the SDK to avoid real API calls in tests
vi.mock("webflow-api", () => ({
  WebflowClient: vi.fn().mockImplementation(() => ({
    sites: {
      list: vi.fn().mockResolvedValue({
        sites: [
          { id: "site-123", displayName: "Test Site", shortName: "test" },
        ],
      }),
    },
    collections: {
      list: vi.fn().mockResolvedValue({
        collections: [
          {
            id: "col-456",
            displayName: "Blog Posts",
            slug: "blog-posts",
            itemCount: 12,
            fields: [
              { displayName: "Name", slug: "name", type: "PlainText", isRequired: true },
              { displayName: "Slug", slug: "slug", type: "PlainText", isRequired: true },
              { displayName: "Post Body", slug: "post-body", type: "RichText", isRequired: false },
            ],
          },
        ],
      }),
      items: {
        listItems: vi.fn().mockResolvedValue({
          items: [
            { id: "item-789", isDraft: false, fieldData: { name: "Test Post", slug: "test-post" } },
          ],
          pagination: { limit: 100, offset: 0, total: 1 },
        }),
        createItem: vi.fn().mockResolvedValue({
          id: "item-new",
          isDraft: true,
          fieldData: { name: "New Post", slug: "new-post" },
        }),
      },
    },
  })),
}));

describe("Webflow Collections", () => {
  let webflow: WebflowClient;

  beforeEach(() => {
    webflow = new WebflowClient({ accessToken: "test-token" });
  });

  it("should list collections for a site", async () => {
    const { collections } = await webflow.collections.list("site-123");
    expect(collections).toHaveLength(1);
    expect(collections![0].displayName).toBe("Blog Posts");
  });

  it("should list items in a collection", async () => {
    const { items } = await webflow.collections.items.listItems("col-456");
    expect(items).toHaveLength(1);
    expect(items![0].fieldData?.name).toBe("Test Post");
  });

  it("should create a CMS item", async () => {
    const item = await webflow.collections.items.createItem("col-456", {
      fieldData: { name: "New Post", slug: "new-post" },
    });
    expect(item.id).toBe("item-new");
    expect(item.isDraft).toBe(true);
  });
});

Step 6: Webhook Testing with ngrok

# Terminal 1: Start dev server
npm run dev

# Terminal 2: Expose local server
ngrok http 3000

# Copy the https:// URL from ngrok, then register webhook:
curl -X POST https://api.webflow.com/v2/sites/{site_id}/webhooks \
  -H "Authorization: Bearer $WEBFLOW_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "triggerType": "form_submission",
    "url": "https://your-ngrok-url.ngrok.io/webhooks/webflow"
  }'

Output

  • Working dev environment with hot reload (tsx watch)
  • Mocked test suite (no API calls in CI)
  • ngrok tunnel for webhook testing
  • Environment variable management via .env.local

Error Handling

ErrorCauseSolution
WEBFLOW_API_TOKEN not setMissing .env.localcp .env.example .env.local and fill in values
Port 3000 in useAnother process`lsof -ti:3000 \
Test mock type errorSDK version mismatchUpdate mock to match current SDK types
ngrok tunnel expiredFree tier limitRestart ngrok or use paid plan

Resources

Next Steps

See webflow-sdk-patterns for production-ready code patterns.

Info
Category Development
Name webflow-local-dev-loop
Version v20260423
Size 8.1KB
Updated At 2026-04-28
Language