NocoDB API Integration Library

Full-featured NocoDB API client with typed records, pagination, caching, and portfolio-specific transformations.

Claude
Claude Claude

Changelog - 2025-12-24 (#1)

Dark Matter Site: NocoDB API Integration Library

Overview

Implemented a comprehensive NocoDB API client library that fetches portfolio company data from a cloud-hosted NocoDB instance. This replaces static JSON files with dynamic, centralized data management while maintaining a graceful fallback mechanism.

Key deliverables:

  • NocoDB API Client (src/lib/nocodb.ts): Full v3 API integration with typed responses
  • Caching Layer: 5-minute TTL cache to reduce API calls during builds
  • Portfolio Transformations: Convert NocoDB records to render-ready portfolio company format
  • Environment Configuration: Secure API key handling with sensible defaults
  • Graceful Degradation: Falls back to static JSON when API not configured

Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                              Build / SSR Time                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   ┌──────────────┐      ┌─────────────────┐      ┌────────────────────┐    │
│   │   Astro      │      │  nocodb.ts      │      │   NocoDB Cloud     │    │
│   │   Page       │─────▶│  API Client     │─────▶│   (app.nocodb.com) │    │
│   │              │      │                 │      │                    │    │
│   └──────────────┘      └────────┬────────┘      └────────────────────┘    │
│                                  │                                          │
│                                  ▼                                          │
│                         ┌─────────────────┐                                 │
│                         │  Memory Cache   │                                 │
│                         │  (5 min TTL)    │                                 │
│                         └─────────────────┘                                 │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Data Flow:
┌────────────┐     ┌─────────────────┐     ┌──────────────────┐
│  NocoDB    │     │  Transform to   │     │  Portfolio Page  │
│  Record    │────▶│  PortfolioCompany│────▶│  Component Props │
│  (raw API) │     │  (typed struct) │     │  (render-ready)  │
└────────────┘     └─────────────────┘     └──────────────────┘

Changes by Area

1. NocoDB API Client (src/lib/nocodb.ts)

Created a full-featured API client for NocoDB v3 API:

Configuration:

const NOCODB_BASE_URL = 'https://app.nocodb.com';
const DEFAULT_BASE_ID = 'pvop0ydhmtugzvv';

export const NOCODB_TABLES = {
  organizations: 'myxl4ug85sr1d4p',
  materials: 'mruw5fu5cthdwkl',
} as const;

Type Definitions:

export interface NocoDBConfig {
  apiKey: string;
  baseUrl: string;
  baseId: string;
}

export interface NocoDBRecord<T = Record<string, any>> {
  id: number;
  fields: T;
}

export interface NocoDBResponse<T = Record<string, any>> {
  records: NocoDBRecord<T>[];
  nestedNext?: string | null;
}

export interface NocoDBQueryParams {
  where?: string;          // Filter conditions (NocoDB syntax)
  limit?: number;          // Max 1000, default 25
  offset?: number;         // Pagination offset
  sort?: NocoDBSortParam[]; // v3 API JSON array format
  fields?: string[];       // Specific fields to include
  viewId?: string;         // View ID override
}

Core Functions:

Function Description
fetchRecords<T>() Fetch paginated records from a table
fetchAllRecords<T>() Auto-paginate to get all records
isNocoDBConfigured() Check if API key is set
clearNocoDBCache() Clear memory cache
getCacheStats() Debug cache state

2. Caching Layer

Implemented in-memory caching with TTL to minimize API calls:

const recordsCache = new Map<string, { data: NocoDBResponse; expires: number }>();
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes

// Cache key includes table + query params for granular invalidation
const cacheKey = `${tableId}:${JSON.stringify(params)}`;

const cached = recordsCache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
  console.log(`[nocodb] Cache hit for ${tableId}`);
  return cached.data as NocoDBResponse<T>;
}

Benefits:

  • Reduces API calls during iterative development
  • Speeds up multi-page builds that query same data
  • Console logging for cache hit visibility

3. Organization Field Types

Typed the NocoDB organization table schema:

export interface OrganizationFields {
  conventionalName: string;
  officialName: string;
  'Entity-Type'?: string | null;  // LP, C-Corp, LLC, etc.
  uuid?: string | null;
  url?: string | null;
  elevatorPitch?: string | null;
  trademarksSlugs?: TrademarksSlugs | string | null;
  Materials?: { id: number; fields: { Title: string } } | null;
  relatedMaterials?: number;
  CreatedAt: string;
  UpdatedAt: string | null;
}

4. Trademark/Logo Handling

Parse the trademarksSlugs JSON field from NocoDB:

export interface TrademarksSlugs {
  trademarkDarkMode?: string;
  trademarkLightMode?: string;
  trademarkVibrantMode?: string;
}

function parseTrademarksSlugs(
  trademarksSlugs: TrademarksSlugs | string | null | undefined
): { lightMode?: string; darkMode?: string; vibrantMode?: string } {
  // Handle both object and JSON string formats
  let parsed: TrademarksSlugs;
  if (typeof trademarksSlugs === 'string') {
    parsed = JSON.parse(trademarksSlugs);
  } else {
    parsed = trademarksSlugs;
  }

  // Prefix with public asset path
  return {
    lightMode: parsed.trademarkLightMode
      ? `/portfolio/logos/${parsed.trademarkLightMode}`
      : undefined,
    // ...
  };
}

5. Portfolio Company Transformation

Transform raw NocoDB records to render-ready format:

export interface PortfolioCompany {
  id: number;
  conventionalName: string;
  officialName: string;
  entityType: string | null;
  logoLightMode?: string;
  logoDarkMode?: string;
  logoVibrantMode?: string;
  logoIsTransparent?: boolean;  // Needs CSS color treatment
  urlToPortfolioSite?: string;
  blurbShortTxt?: string;
  category?: 'direct' | 'lp';
}

export function transformToPortfolioCompanies(
  records: NocoDBRecord<OrganizationFields>[]
): PortfolioCompany[] {
  return records.map((record) => {
    const { fields } = record;

    // Derive category from Entity-Type
    // LP = fund investment, C-Corp/LLC = direct investment
    const entityType = fields['Entity-Type'] || null;
    let category: 'direct' | 'lp' = 'direct';
    if (entityType === 'LP') {
      category = 'lp';
    }

    // Parse logos and detect transparent SVGs
    const trademarks = parseTrademarksSlugs(fields.trademarksSlugs);
    const logoIsTransparent = Boolean(
      trademarks.lightMode?.includes('--Transparent') ||
      trademarks.darkMode?.includes('--Transparent')
    );

    return {
      id: record.id,
      conventionalName: fields.conventionalName,
      category,
      logoLightMode: trademarks.lightMode || '/portfolio/logos/placeholder-light.svg',
      logoDarkMode: trademarks.darkMode || '/portfolio/logos/placeholder-dark.svg',
      logoIsTransparent,
      urlToPortfolioSite: normalizeUrl(fields.url),
      blurbShortTxt: fields.elevatorPitch || undefined,
      // ...
    };
  });
}

6. URL Normalization

Handle URLs that may lack protocol prefix:

function normalizeUrl(url: string | null | undefined): string | undefined {
  if (!url) return undefined;
  const trimmed = url.trim();
  if (!trimmed) return undefined;

  // Already has protocol
  if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
    return trimmed;
  }

  // Add https:// prefix
  return `https://${trimmed}`;
}

7. Environment Configuration

Added NocoDB configuration to .env.example:

# NocoDB Integration
# ------------------
# API key for fetching portfolio data from NocoDB
# Generate at: NocoDB > Account Settings > Tokens
NOCODB_API_KEY=your_nocodb_api_key_here

# Optional: Override defaults
# NOCODB_BASE_URL=https://app.nocodb.com
# NOCODB_BASE_ID=pvop0ydhmtugzvv

Files Changed Summary

New

  • src/lib/nocodb.ts — Complete NocoDB API client with types, caching, and transformations

Modified

  • .env.example — Added NocoDB configuration variables with documentation
  • .claude/settings.local.json — Added nocodb.com to allowed WebFetch domains

Technical Details

NocoDB v3 API Format

The v3 API uses a different structure than earlier versions:

GET /api/v3/data/{baseId}/{tableId}/records
Headers:
  Accept: application/json
  xc-token: {apiKey}

Query params:
  limit=100
  offset=0
  sort=[{"field":"conventionalName","direction":"asc"}]
  where=(field,eq,value)

API Response Structure

{
  "records": [
    {
      "id": 12345,
      "fields": {
        "conventionalName": "Example Corp",
        "Entity-Type": "C-Corp",
        "url": "example.com",
        "trademarksSlugs": "{\"trademarkDarkMode\":\"trademark__Example--Dark-Mode.svg\"}"
      }
    }
  ],
  "nestedNext": null
}

Authentication Pattern

Uses xc-token header for API authentication:

const response = await fetch(url.toString(), {
  headers: {
    'Accept': 'application/json',
    'xc-token': config.apiKey,
  },
});

Error Handling

Graceful degradation on API failures:

try {
  const response = await fetch(url.toString(), { /* ... */ });

  if (!response.ok) {
    const errorBody = await response.text();
    console.error(`[nocodb] API error: ${response.status}`, errorBody);
    throw new Error(`NocoDB API error: ${response.status}`);
  }

  return await response.json();
} catch (error) {
  console.error(`[nocodb] Failed to fetch records:`, error);
  return { records: [], nestedNext: null };  // Empty result, not crash
}

Notes / Follow-Ups

  1. Pagination: fetchAllRecords() handles pagination automatically, but consider adding a max records limit for safety.

  2. Cache Invalidation: Current cache is in-memory and resets on dev server restart. Consider adding manual cache clear endpoint for production.

  3. Field Mapping: The Entity-Type field uses bracket notation due to hyphen. Consider aliasing in NocoDB for cleaner access.

  4. Linked Records: The Materials field shows NocoDB's linked record structure. Could expand to fetch full material details.

  5. Rate Limiting: NocoDB cloud has rate limits. The caching layer helps, but monitor usage in production.

  6. Future Tables: The NOCODB_TABLES constant is ready for additional tables (materials, team, etc).