This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
ParksAPI is a TypeScript library for fetching real-time theme park data (wait times, schedules, entities) from 75+ park implementations, serving ~118 destination entities worldwide. All park implementations live in src/parks/.
Key Requirement: Node 24+, npm 11+
Detailed implementation guides live in .claude/ — always check these before writing park code:
.claude/skills/implementing-parks.md— Complete guide for building new park destinations: scaffold, decorators, entity mapping, live data, schedules, validation workflow, tips & tricks, and 12 reference implementations.claude/agents/park-ts-migrator.md— Agent for migrating JS park implementations to TypeScript
npm run build # Compile TypeScript to dist/
npm run watch # Auto-recompile on changes
npm run dev # Test all parks via harness
npm run dev -- <id> # Test specific park (e.g. universalorlando)
npm run dev -- --list # List available park IDs
npm test # Run Vitest tests
npm run test:coverage # Coverage report
npm run health # Health check all endpoints
npm run har -- <file> # Analyse HAR captureAutomatic configuration loading. Priority: instance config > env vars > prefixed env vars > defaults.
Property decorator marks properties for config injection. @destinationController auto-applies the class-level @config — never add @config to a class that uses @destinationController.
@destinationController({category: 'MyCategory'})
export class MyPark extends Destination {
@config apiKey: string = ''; // Reads MYPARK_APIKEY env var
@config timeout: number = 5000; // Reads MYPARK_TIMEOUT, coerced to number
@config enabled: boolean = true; // Reads MYPARK_ENABLED, coerced to boolean
constructor(options?: DestinationConstructor) {
super(options);
this.addConfigPrefix('MYPARK'); // Also checks MYPARK_* env vars
}
}Type coercion: Env vars are strings, but @config automatically coerces to match the default value's type:
numberdefaults: parsed viaNumber(), falls back to default ifNaNbooleandefaults: case-insensitive"true"/"1"→true, elsefalsestringdefaults: no coercion
No hardcoded URLs or secrets — use empty string defaults ('') and configure via .env.
SQLite-backed caching with TTL support. Database: ./cache.sqlite (override with CACHE_DB_PATH).
@cache({ttlSeconds: 60 * 60 * 3}) // 3 hours
async getParks() { ... }
@cache({callback: (resp) => resp?.expiresIn || 3600}) // Dynamic TTL
async getAPIKey() { ... }Direct access: CacheLib.get(), CacheLib.set(), CacheLib.wrap(), CacheLib.delete(), CacheLib.clearByClassName(), CacheLib.clearAll()
In-flight deduplication: CacheLib.wrap() deduplicates concurrent cache misses — only one caller executes the function, others wait for the result.
Cache key collisions in base classes: When multiple parks share a base class, use getCacheKeyPrefix() to prevent collisions:
getCacheKeyPrefix(): string {
return `attractionsio:${this.parkId}`;
}
// Keys become: attractionsio:1:ClassName:methodName:[args]Alternatives: cacheKeyPrefix property, unique method arguments, or custom key function. See Cedar Fair (src/parks/cedarfair/attractionsio.ts) for reference.
Cache versioning (invalidate stale shapes on deploy): When a method's cached result shape changes — new fields, different grouping, different meaning — bump cacheVersion so old entries become unreachable (they still sit in SQLite until TTL but are never looked up). No manual flush needed across machines.
@cache({ttlSeconds: 86400, cacheVersion: 2}) // method-level
async getParkData() { ... }Class-level fallback applies to every @cache method in the class unless the method sets its own version:
class MyPark extends Destination {
protected cacheVersion = 3; // property form
// or: getCacheVersion(method: string) { return … } // method form (per-method)
@cache({ttlSeconds: 60}) async a() { … } // keyed under :v3
@cache({ttlSeconds: 60, cacheVersion: 9}) async b() { … } // keyed under :v9
}Precedence: method option > class getCacheVersion() > class cacheVersion > unversioned.
Queue-based HTTP with automatic retry, validation, and caching.
@http({cacheSeconds: 180, retries: 3})
async fetchParks(): Promise<HTTPObj> {
return {
method: 'GET',
url: `${this.baseURL}/venues`,
options: {json: true},
tags: ['parks'],
} as HTTPObj;
}Key: uses node:http/node:https (not fetch), global queue with 100ms interval, 250ms rate limit, request deduplication.
Event-based dependency injection using Sift.js (MongoDB-like queries). Used for auth headers, response transforms.
@inject({
eventName: 'httpRequest',
hostname: function() { return hostnameFromUrl(this.apiBase); },
tags: {$nin: ['auth']},
})
async injectAuth(req: HTTPObj): Promise<void> {
const {token} = await this.getToken();
req.headers = {...req.headers, 'Authorization': `Bearer ${token}`};
}Priority: Lower number runs first. Default is 0. Same priority runs in parallel.
Promise reuse to prevent duplicate async execution. @reusable({forever: true}) for singletons.
All parks extend Destination using the Template Method Pattern.
Public API (DO NOT OVERRIDE — marked @final in base class): getEntities(), getLiveData(), getSchedules(). These call init(), then delegate to the corresponding build*() method, then run hierarchy resolution / sanitisation / validation.
Public API (OVERRIDE TO PROVIDE):
getDestinations()— Return the top-levelDESTINATIONentity (or entities). Default returns[]. Common pattern across parks.
Protected API (IMPLEMENT IN SUBCLASSES):
_init()— Optional one-time initializationbuildEntityList()— Return entities (hierarchy auto-resolved from parent chains)buildLiveData()— Return live data (wait times, statuses)buildSchedules()— Return schedules (operating hours)
Helper methods: mapEntities(), resolveEntityHierarchy(), getLocalizedString(), buildReturnTimeQueue(), buildBoardingGroupQueue(), calculateReturnWindow()
Lifecycle: init() auto-called before any data method, runs once via @reusable({forever: true}).
All types come from @themeparks/typelib: Entity, LiveData, EntitySchedule, TagType, LocalisedString, LanguageCode, AttractionTypeEnum, QueueType.
src/datetime.ts—constructDateTime(),formatInTimezone(),localFromFakeUtc(),hostnameFromUrl(),addDays(),addMinutes()src/tags/—TagBuilderAPI for entity metadata (height restrictions, accessibility). Seesrc/tags/TAG_DEVELOPMENT_GUIDE.mdsrc/virtualQueue/—VQueueBuilderfor return times, boarding groups, paid queues. Seesrc/virtualQueue/VIRTUAL_QUEUE_GUIDE.mdsrc/htmlUtils.ts—stripHtmlTags(),decodeHtmlEntities()src/statusMap.ts—createStatusMap()for mapping API statuses to standard valuessrc/wsUtils.ts— WebSocket async iterator for live stream support
Per-destination HTTP proxy for routing through CrawlBase, Scrapfly, or basic HTTP proxies.
Global (all destinations): Set GLOBAL_CRAWLBASE, GLOBAL_SCRAPFLY, or GLOBAL_BASICPROXY env vars. Auto-detected on first destination instantiation.
Per-destination: Each time a destination registers a config prefix via addConfigPrefix('MYPARK'), the matching env vars (MYPARK_CRAWLBASE, MYPARK_SCRAPFLY, MYPARK_BASICPROXY) are auto-loaded and merged into proxyConfig. No opt-in required — if the env var is set, the proxy is used. Consumers can also assign destInstance.proxyConfig directly after construction for fully explicit wiring.
Priority: CrawlBase > Scrapfly > Basic proxy. Per-destination overrides global. Proxy injection runs at priority 999 (after all auth/header injectors). Note: CrawlBase/Scrapfly rewrite URLs and are scraping services — they don't forward custom headers or POST bodies. Use BASICPROXY for authenticated API proxying.
@destinationController auto-registers destinations and applies @config. ID and name derived from class name:
@destinationController({category: 'Universal'})
export class UniversalOrlando extends Destination {
// ID: 'universalorlando', Name: 'Universal Orlando'
}No manual imports needed — the test harness auto-discovers all .ts/.js files in src/parks/.
Location: src/**/__tests__/**/*.test.ts | Runner: Vitest | Config: vitest.config.ts
Parks are excluded from coverage (integration-tested via npm run dev). Core libraries target 85-100%.
HTTP queue in tests: Use waitForHttpQueue() and stopHttpQueue() from ../http to ensure queued requests complete before assertions.
.claude/skills/implementing-parks.md— How to build a new park destinationsrc/tags/TAG_DEVELOPMENT_GUIDE.md— Adding new tag typessrc/virtualQueue/VIRTUAL_QUEUE_GUIDE.md— Virtual queue patterns with real-world examplesTODO.MD— Known issues, code review findings
- Entity IDs: Always strings, even if sourced from numbers
- Template Method: Implement
build*()methods, NOTget*()methods - Hierarchy: Automatic —
parkId/destinationIdresolved from parent chains - No hardcoded URLs/secrets: All in
@configwith empty defaults, loaded from.env - No
@configon classes:@destinationControllerhandles it. Only use@configon properties - Config lookup order: Instance config >
{CLASSNAME}_{PROPERTY}>{PREFIX}_{PROPERTY}> default - HTTP: Uses
node:http/node:https(notfetch) for proxy consistency - Cache: Only cache JSON-safe types (no Set, Map, Date objects)
- Numeric validation: Never use
isNaN()on values from external APIs —isNaN("")returnsfalsedue to JS coercion. UseNumber.isFinite()afterNumber(), orparseInt()/parseFloat()which returnNaNfor empty strings.waitTimemust always be a finite number or null/undefined — the base classgetLiveData()sanitises output as a safety net. - TypeScript: ES2022, strict mode, decorators enabled, ESM modules