Skip to content

feat: new Farcaster Express API + Turborepo restructure - #18

Merged
dylsteck merged 28 commits into
mainfrom
feat/data-layer-implementation
Mar 7, 2026
Merged

feat: new Farcaster Express API + Turborepo restructure#18
dylsteck merged 28 commits into
mainfrom
feat/data-layer-implementation

Conversation

@dylsteck

@dylsteck dylsteck commented Mar 7, 2026

Copy link
Copy Markdown
Owner

Summary

Creates a unified Farcaster API that proxies all our third-party requests (Neynar, Snapchain, Farcaster API, Optimism RPC). Caching, request coalescing, and a single backend—simpler frontend, fewer duplicate calls. Also restructures the repo as a Turborepo monorepo.

  • apps/api — Express server with Redis cache and request coalescing
  • apps/web — Casterscan Next.js app (moved from root)
  • MigrationuseFidStats, useSignerMessages, useAppsWithSigners now hit the API via Next.js proxy routes

Architecture

flowchart TB
    subgraph Client["Browser"]
        WebApp["Next.js (apps/web)"]
    end

    subgraph NextJS["Next.js API Routes"]
        ProxyStats["/api/fid/[fid]/stats"]
        ProxyEnriched["/api/signers/enriched"]
        ProxyMessages["/api/signers/messages"]
    end

    subgraph DataLayer["Data Layer (apps/api)"]
        direction TB
        Cache["Redis Cache"]
        Coalesce["Request Coalescing"]
        Routes["Express Routes"]
        Routes --> Cache
        Routes --> Coalesce
    end

    subgraph Upstream["Upstream APIs"]
        Neynar["Neynar API"]
        Snapchain["Snapchain REST"]
        Farcaster["Farcaster API"]
        Optimism["Optimism RPC"]
    end

    WebApp --> ProxyStats
    WebApp --> ProxyEnriched
    WebApp --> ProxyMessages

    ProxyStats --> DataLayer
    ProxyEnriched --> DataLayer
    ProxyMessages --> DataLayer

    DataLayer --> Neynar
    DataLayer --> Snapchain
    DataLayer --> Farcaster
    DataLayer --> Optimism
Loading

Data Flow (Profile Page Example)

sequenceDiagram
    participant User
    participant Web
    participant Proxy
    participant API
    participant Redis
    participant Snapchain
    participant Neynar

    User->>Web: Visit /fids/3
    Web->>Proxy: GET /api/fid/3/stats
    Proxy->>API: GET /v1/fids/3/stats
    API->>Redis: Check cache
    alt Cache hit
        Redis-->>API: Return cached
    else Cache miss
        API->>Snapchain: getCastsByFid, getReactionsByFid, etc.
        Snapchain-->>API: Raw data
        API->>Redis: Store (TTL 15min)
    end
    API-->>Proxy: { casts, reactions, links, verifications }
    Proxy-->>Web: JSON
    Web->>Proxy: GET /api/signers/enriched?fid=3
    Proxy->>API: GET /v1/fids/3/signers/enriched
    API->>Snapchain: getAllCastsByFid, getOnChainSignersByFid, etc.
    API->>Neynar: getUsers (app profiles)
    API-->>Proxy: Apps with signers
    Proxy-->>Web: JSON
    Web-->>User: Render profile
Loading

Monorepo Structure

flowchart LR
    subgraph Root["/"]
        Turbo["turbo.json"]
        Pkg["package.json"]
    end

    subgraph Apps["apps/"]
        Web["web/ (Next.js)"]
        API["api/ (Express)"]
    end

    subgraph WebContents["apps/web"]
        App["app/"]
        Public["public/"]
        NextConfig["next.config.js"]
    end

    subgraph APIContents["apps/api"]
        Src["src/"]
        Cache["cache/"]
        Upstream["upstream/"]
        Routes["routes/"]
        Services["services/"]
    end

    Pkg --> Web
    Pkg --> API
    Turbo --> Web
    Turbo --> API
Loading

API Surface (Data Layer)

Method Path Description
GET /health Health check
GET /v1/fids/:fid/stats Cast/reaction/link/verification counts
GET /v1/fids/:fid/messages All messages by FID
GET /v1/fids/:fid/signers/enriched Signers + app profiles
GET /v1/fids/:fid/signers/:signer/messages Messages filtered by signer
GET /v1/fids/:fid/signers/:signer/stats Signer stats
GET /v1/fids/:fid/keys Auth addresses + signer keys
GET /v1/users/:fid User by FID
GET /v1/users/by-username/:username User by username
POST /v1/users/bulk Bulk users
GET /v1/casts/:hash Enriched cast (+ ?format= for hub variants)
GET /v1/snapchain/info Snapchain node info
GET /v1/snapchain/events/:eventId Snapchain event by ID

Migrated Hooks

Hook Before After
useFidStats 4 parallel fetches to /api/farcaster/[fid]/* 1 fetch to /api/fid/[fid]/stats
useSignerMessages Client-side Snapchain (4 paginated calls) 1 fetch to /api/signers/messages
useAppsWithSigners /api/signers/enriched (direct Snapchain + Neynar) Same route, now proxies to data layer

Commands

Command Description
bun run dev Start web + API in parallel (ports 3000, 4000)
bun run dev:api Start API only (port 4000)
bun run dev:web Start web only (port 3000)
bun run build Build both apps
bun run start Start web (after build)

Env Vars

  • NEYNAR_API_KEY — Required for user/cast lookups (API only)
  • API_URL — For web app, points to API (default http://localhost:4000 dev, https://api.casterscan.com prod)
  • REDIS_URL — Optional; cache disabled if unset (API only)
  • ALLOWED_ORIGINS — Optional; CORS allowlist, comma-separated (API only). Default: casterscan.com, localhost:3000

Technical Notes

  • Express on Vercel — API migrated from Elysia to Express for reliable Vercel deployment (docs)
  • ESM — All relative imports use explicit .js extensions for Node.js ESM compatibility
  • Validation — Zod schemas for type-safe params, query, and body validation
  • Security — CORS allowlist, rate limiting, path traversal protection, strict input validation (fid, hash, eventId, etc.)

Repository owner deleted a comment from cursor Bot Mar 7, 2026
@dylsteck
dylsteck force-pushed the feat/data-layer-implementation branch 4 times, most recently from 3ef25c7 to e4ff9c3 Compare March 7, 2026 23:36
@dylsteck dylsteck changed the title feat: Farcaster data layer + Turborepo restructure feat: new Farcaster Express API + Turborepo restructure Mar 7, 2026
cursoragent and others added 24 commits March 7, 2026 18:53
Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
- Add packages/data-layer: Elysia server with Redis cache, request coalescing
- Upstream clients: Neynar, Snapchain, Farcaster API, Optimism keys
- Routes: /v1/fids/:fid/stats, messages, signers/enriched, signers/:signer/messages|stats
- Routes: /v1/users, /v1/casts, /v1/fids/:fid/keys, /v1/snapchain/info
- Next.js proxies: /api/fid/[fid]/stats, /api/signers/enriched, /api/signers/messages
- Migrate useFidStats, useSignerMessages, useAppsWithSigners to data layer
- Add CI workflow for Next.js + data-layer build
- Add DATA_LAYER_URL to .env.example

Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
- Move Next.js app to apps/web
- Move data layer to apps/api
- Add turbo.json, Turborepo scripts
- Root package.json: workspaces apps/*, packages/*
- Update CI, README, .gitignore
- dev, dev:api, dev:all, build all use turbo

Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
…API-only

- lib/server.ts: getNeynarCast, getNeynarUser, getNeynarUserByUsername, getFarcasterKeys now use data layer
- api/neynar/*, api/snapchain/cast: proxy to data layer
- .env.example: NEYNAR_API_KEY and REDIS_URL are API-only; DATA_LAYER_URL is web-only

Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
- Rename data-layer.ts to api.ts, dataLayerFetch to apiFetch, DATA_LAYER_URL to API_URL
- Proxy all web API routes to apps/api (snapchain, farcaster, signers, neynar)
- Add /v1/fids/:fid/signers to API for raw on-chain signers
- Remove direct snapchain/farcaster/neynar libs from web (dead code)
- Update .env.example and README

Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
…fore export)

Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
…rror

Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
… not instantiated)

Co-authored-by: Dylan Steck <dylan.steck@coinbase.com>
…ibility

Node.js ESM requires explicit file extensions in relative imports.
ERR_MODULE_NOT_FOUND was caused by imports like './lib/errors' without .js
dylsteck added 4 commits March 7, 2026 18:53
- Cache: fidSignersEnriched 1hr, remove VERY_LONG, align Redis/web TTLs
- Security: strict validation (fid, hash, eventId, etc), path traversal middleware
- Security: stop leaking err.message, sanitize API errors in prod
- Security: CORS allowlist, rate limiting, NEYNAR_API_KEY required in prod
- Security: cache key sanitization, web proxy validation
- Bug: fix frame URL profiles→fids
- Quality: remove dead Skeleton.tsx, proxy.ts
- Quality: replace any types, add PruneEvent type
@dylsteck
dylsteck force-pushed the feat/data-layer-implementation branch from 7bb4916 to 1c2d1ca Compare March 7, 2026 23:53
@dylsteck
dylsteck merged commit f0a3dec into main Mar 7, 2026
@dylsteck
dylsteck deleted the feat/data-layer-implementation branch March 7, 2026 23:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants