Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Configure Firecrawl local development with self-hosted Docker, mocking, and testing. Use when setting up a development environment, running Firecrawl locally to save credits, or configuring test workflows with vitest. Trigger with phrases like "firecrawl dev setup", "firecrawl local development", "firecrawl docker", "firecrawl self-hosted dev", "firecrawl test setup".
.claude/skills/jeremylongshore-firecrawl-local-dev-loop/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 21% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 96% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 21% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 23% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 60% | 0% |
Set up a fast development workflow for Firecrawl integrations. Use self-hosted Firecrawl via Docker to avoid burning API credits during development, mock the SDK for unit tests, and run integration tests against the local instance.
@mendable/firecrawl-js installedmy-firecrawl-project/
├── src/
│ ├── scraper.ts # Firecrawl business logic
│ └── config.ts # Environment-aware config
├── tests/
│ ├── scraper.test.ts # Unit tests (mocked SDK)
│ └── integration.test.ts # Integration tests (real API)
├── docker-compose.yml # Self-hosted Firecrawl
├── .env.local # Dev secrets (git-ignored)
├── .env.example # Template for team
└── package.jsonyaml# docker-compose.yml services: firecrawl: image: mendableai/firecrawl:latest ports: - "3002:3002" environment: - PORT=3002 - USE_DB_AUTHENTICATION=false - REDIS_URL=redis://redis:6379 - NUM_WORKERS_PER_QUEUE=1 - BULL_AUTH_KEY=devonly depends_on: redis: condition: service_healthy redis: image: redis:7-alpine ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5
bashset -euo pipefail # Start local Firecrawl docker compose up -d # Verify it's running curl -s http://localhost:3002/health | jq .
typescript// src/config.ts import FirecrawlApp from "@mendable/firecrawl-js"; export function getFirecrawl(): FirecrawlApp { const isDev = process.env.NODE_ENV !== "production"; return new FirecrawlApp({ apiKey: process.env.FIRECRAWL_API_KEY || "fc-dev", // Point to local Docker instance in dev ...(isDev && process.env.FIRECRAWL_API_URL ? { apiUrl: process.env.FIRECRAWL_API_URL } : {}), }); }
bash# .env.local (for development — zero API credits used) FIRECRAWL_API_KEY=fc-localdev FIRECRAWL_API_URL=http://localhost:3002 NODE_ENV=development
typescript// tests/scraper.test.ts import { describe, it, expect, vi, beforeEach } from "vitest"; // Mock the SDK vi.mock("@mendable/firecrawl-js", () => ({ default: vi.fn().mockImplementation(() => ({ scrapeUrl: vi.fn().mockResolvedValue({ success: true, markdown: "# Hello World\n\nSample content from mock", metadata: { title: "Hello World", sourceURL: "https://example.com" }, }), crawlUrl: vi.fn().mockResolvedValue({ success: true, data: [ { markdown: "# Page 1", metadata: { sourceURL: "https://example.com/page1" }, }, ], }), mapUrl: vi.fn().mockResolvedValue({ success: true, links: ["https://example.com/a", "https://example.com/b"], }), })), })); import { scrapeAndProcess } from "../src/scraper"; describe("Scraper", () => { it("returns cleaned markdown", async () => { const result = await scrapeAndProcess("https://example.com"); expect(result.markdown).toContain("Hello World"); expect(result.metadata.title).toBe("Hello World"); }); });
typescript// tests/integration.test.ts import { describe, it, expect } from "vitest"; import FirecrawlApp from "@mendable/firecrawl-js"; const FIRECRAWL_URL = process.env.FIRECRAWL_API_URL || "http://localhost:3002"; describe.skipIf(!process.env.FIRECRAWL_API_URL)("Firecrawl Integration", () => { const firecrawl = new FirecrawlApp({ apiKey: "fc-test", apiUrl: FIRECRAWL_URL, }); it("scrapes a page to markdown", async () => { const result = await firecrawl.scrapeUrl("https://example.com", { formats: ["markdown"], }); expect(result.success).toBe(true); expect(result.markdown).toBeDefined(); expect(result.markdown!.length).toBeGreaterThan(50); }, 30000); });
json{ "scripts": { "dev": "tsx watch src/index.ts", "test": "vitest", "test:watch": "vitest --watch", "test:integration": "FIRECRAWL_API_URL=http://localhost:3002 vitest run tests/integration", "firecrawl:up": "docker compose up -d", "firecrawl:down": "docker compose down", "firecrawl:logs": "docker compose logs -f firecrawl" } }
localhost:3002tsx watch| Error | Cause | Solution | |-------|-------|----------| | Docker ECONNREFUSED | Container not running | docker compose up -d | | Redis connection refused | Redis not healthy yet | Wait for healthcheck, retry | | MODULE_NOT_FOUND | Missing dependency | npm install @mendable/firecrawl-js | | Integration test timeout | Self-hosted Firecrawl slow | Increase vitest timeout to 30s | | Port 3002 in use | Another process | lsof -i :3002 and kill, or change port |
typescript// scripts/dev-scrape.ts import { getFirecrawl } from "../src/config"; const firecrawl = getFirecrawl(); const result = await firecrawl.scrapeUrl(process.argv[2] || "https://example.com", { formats: ["markdown"], }); console.log(result.markdown);
bashnpx tsx scripts/dev-scrape.ts https://docs.firecrawl.dev
See firecrawl-sdk-patterns for production-ready code patterns.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 31,610 | 19,502 | -38% | 1 | 1 | 0% | 5,628 | 4,992 | -11% | 0 | 0 | — |
case-02 | fail→fail | 17,069 | 18,735 | +10% | 1 | 1 | 0% | 3,690 | 4,930 | +34% | 0 | 0 | — |
case-03 | fail→pass | 25,552 | 20,990 | -18% | 1 | 1 | 0% | 4,391 | 5,299 | +21% | 0 | 0 | — |
case-04 | pass→pass | 22,826 | 26,190 | +15% | 1 | 1 | 0% | 3,489 | 5,970 | +71% | 0 | 0 | — |
case-05 | pass→pass | 27,904 | 21,932 | -21% | 1 | 1 | 0% | 4,376 | 5,765 | +32% | 0 | 0 | — |
case-06 | pass→pass | 24,339 | 24,912 | +2% | 1 | 1 | 0% | 3,635 | 5,871 | +62% | 0 | 0 | — |
case-07 | fail→pass | 8,392 | 5,901 | -30% | 1 | 1 | 0% | 1,475 | 2,890 | +96% | 0 | 0 | — |
case-08 | fail→pass | 12,885 | 9,531 | -26% | 1 | 1 | 0% | 2,283 | 2,757 | +21% | 0 | 0 | — |
case-09 | pass→pass | 10,866 | 12,506 | +15% | 1 | 1 | 0% | 2,055 | 3,323 | +62% | 0 | 0 | — |
case-10 | fail→pass | 9,235 | 7,654 | -17% | 1 | 1 | 0% | 1,853 | 2,273 | +23% | 0 | 0 | — |
case-11 | fail→pass | 10,938 | 13,750 | +26% | 1 | 1 | 0% | 2,164 | 3,458 | +60% | 0 | 0 | — |
case-12 | fail→pass | 10,590 | 15,699 | +48% | 1 | 1 | 0% | 2,094 | 3,978 | +90% | 0 | 0 | — |
case-13 | fail→pass | 18,411 | 3,790 | -79% | 1 | 1 | 0% | 2,399 | 2,537 | +6% | 0 | 0 | — |
case-14 | pass→pass | 14,058 | 8,272 | -41% | 1 | 1 | 0% | 2,670 | 3,426 | +28% | 0 | 0 | — |
case-15 | pass→pass | 14,035 | 16,017 | +14% | 1 | 1 | 0% | 2,985 | 4,221 | +41% | 0 | 0 | — |
case-16 | pass→pass | 15,504 | 9,980 | -36% | 1 | 1 | 0% | 1,942 | 3,933 | +103% | 0 | 0 | — |
case-17 | pass→pass | 15,139 | 8,043 | -47% | 1 | 1 | 0% | 1,814 | 2,410 | +33% | 0 | 0 | — |
case-18 | pass→pass | 14,020 | 7,526 | -46% | 1 | 1 | 0% | 1,576 | 2,300 | +46% | 0 | 0 | — |
case-19 | pass→pass | 11,963 | 7,969 | -33% | 1 | 1 | 0% | 1,316 | 2,367 | +80% | 0 | 0 | — |
case-20 | fail→pass | 10,697 | 10,551 | -1% | 1 | 1 | 0% | 1,826 | 2,960 | +62% | 0 | 0 | — |
case-21 | fail→pass | 11,881 | 8,632 | -27% | 1 | 1 | 0% | 2,408 | 3,505 | +46% | 0 | 0 | — |
case-22 | fail→fail | 24,961 | 18,534 | -26% | 1 | 1 | 0% | 4,171 | 4,787 | +15% | 0 | 0 | — |
case-23 | pass→pass | 4,184 | 9,101 | +118% | 1 | 1 | 0% | 849 | 2,393 | +182% | 0 | 0 | — |
case-24 | fail→pass | 12,378 | 7,490 | -39% | 1 | 1 | 0% | 1,415 | 2,342 | +66% | 0 | 0 | — |
case-25 | fail→pass | 15,903 | 5,564 | -65% | 1 | 1 | 0% | 2,265 | 3,052 | +35% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +44 percentage points is the difference between those two pass rates over the 25 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.