Appfolio sdk patterns
425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill appfolio-sdk-patternsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
'Apply production-ready patterns for AppFolio REST API integration.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
4.5 KB, as published. Nobody here has run it
AppFolio SDK Patterns
Overview
Production-ready patterns for the AppFolio property management REST API. AppFolio uses HTTP Basic Auth with client credentials and returns JSON responses for properties, tenants, leases, and work orders. A structured singleton client prevents credential sprawl, enforces consistent error handling, and centralizes pagination logic across all property management endpoints.
Singleton Client
import axios, { AxiosInstance } from 'axios';
let _client: AxiosInstance | null = null;
export function getClient(): AxiosInstance {
if (!_client) {
const clientId = process.env.APPFOLIO_CLIENT_ID;
const clientSecret = process.env.APPFOLIO_CLIENT_SECRET;
const baseURL = process.env.APPFOLIO_BASE_URL;
if (!clientId || !clientSecret || !baseURL) throw new Error('APPFOLIO_CLIENT_ID, SECRET, and BASE_URL required');
_client = axios.create({ baseURL, auth: { username: clientId, password: clientSecret }, timeout: 30000 });
}
return _client;
}
Error Wrapper
export class AppFolioError extends Error {
constructor(public status: number, public code: string, message: string) { super(message); }
}
export async function safeCall<T>(operation: string, fn: () => Promise<T>): Promise<T> {
try { return await fn(); }
catch (err: any) {
const status = err.response?.status ?? 0;
if (status === 429) { await new Promise(r => setTimeout(r, 5000)); return fn(); }
if (status === 401) throw new AppFolioError(401, 'AUTH', 'Invalid APPFOLIO_CLIENT_ID or SECRET');
throw new AppFolioError(status, 'API_ERROR', `${operation} failed [${status}]: ${err.message}`);
}
}
Request Builder
class AppFolioQuery {
private params: Record<string, string> = {};
status(s: 'active' | 'past' | 'future') { this.params.status = s; return this; }
propertyId(id: string) { this.params.property_id = id; return this; }
page(n: number) { this.params.page = String(n); return this; }
perPage(n: number) { this.params.per_page = String(Math.min(n, 200)); return this; }
since(date: string) { this.params.updated_since = date; return this; }
build() { return this.params; }
}
// Usage: new AppFolioQuery().status('active').perPage(50).build();
Response Types
interface Property {
id: string; name: string; property_type: 'residential' | 'commercial' | 'mixed';
address: { street: string; city: string; state: string; zip: string };
unit_count: number; status: string;
}
interface Tenant {
id: string; first_name: string; last_name: string;
email: string; phone: string; unit_id: string; lease_id: string;
}
interface Lease {
id: string; unit_id: string; tenant_id: string;
start_date: string; end_date: string; rent_amount: number; status: 'active' | 'expired' | 'future';
}
interface WorkOrder {
id: string; property_id: string; unit_id: string;
description: string; priority: 'low' | 'medium' | 'high' | 'emergency';
status: 'open' | 'in_progress' | 'completed';
}
Testing Utilities
export function mockProperty(overrides: Partial<Property> = {}): Property {
return { id: 'prop-001', name: 'Maple Ridge Apts', property_type: 'residential',
address: { street: '100 Main St', city: 'Austin', state: 'TX', zip: '78701' },
unit_count: 24, status: 'active', ...overrides };
}
export function mockLease(overrides: Partial<Lease> = {}): Lease {
return { id: 'lease-001', unit_id: 'unit-001', tenant_id: 'ten-001',
start_date: '2025-01-01', end_date: '2026-01-01', rent_amount: 1500, status: 'active', ...overrides };
}
Error Handling
| Pattern | When to Use | Example |
|---|---|---|
safeCall wrapper | All API calls | Prevents uncaught 4xx/5xx from crashing flows |
| Retry on 429 | Rate-limited batch imports | Reads Retry-After header for backoff |
| Auth validation | Client init | Throws early if credentials are missing |
| Pagination loop | Listing properties/tenants | Increment page until empty response |
Resources
Next Steps
Apply patterns in appfolio-core-workflow-a.