Skip to content
Skillv1.0.0

appfolio-sdk-patterns

Apply production-ready patterns for AppFolio REST API integration. Trigger: "appfolio patterns".

by jeremylongshore(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from jeremylongshore/tons-of-skills-marketplace (plugins/saas-packs/appfolio-pack/skills/appfolio-sdk-patterns/SKILL.md). Install upstream with npx skills add jeremylongshore/tons-of-skills-marketplace --skill appfolio-sdk-patterns. Copyright stays with the author (MIT).

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.

Prerequisites

  • A provider-verified base URL, auth method, endpoint scope, and managed secret injection path for the target portfolio.
  • Schema validation, request timeouts, endpoint-specific rate limits, and idempotency keys for all mutation-capable service methods.
  • Synthetic fixtures for unit testing; production tenant, lease, and payment payloads must not become default mock, log, or error-message content.

Instructions

  1. Construct one contract-bound client per runtime and reject missing or invalid configuration before requests begin.
  2. Validate query bounds and response shapes at the service boundary, then pass only minimized typed fields to callers.
  3. Return a classified 429 or unknown-write failure to the caller; retry only idempotent operations after the caller records the cursor/key and delay.
  4. Keep write workflows behind explicit authorization, audit, and reconciliation controls rather than embedding them in generic SDK convenience helpers.

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) {
      const retryAfter = err.response?.headers?.['retry-after'];
      throw new AppFolioError(429, 'RATE_LIMIT', `Retry after ${retryAfter ?? 'provider-directed delay'}; do not replay an unknown write automatically`);
    }
    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
Caller-owned retry on 429 Rate-limited idempotent batch reads Preserve cursor/key, honor Retry-After, then retry deliberately
Auth validation Client init Throws early if credentials are missing
Pagination loop Listing properties/tenants Increment page until empty response

Output

  • One validated, contract-bound API client with centralized timeout and error classification behavior
  • Bounded query parameters and minimized typed responses for service callers
  • Explicit rate-limit and unknown-write outcomes that require caller-owned cursor/idempotency/reconciliation decisions

Examples

For a property-list read, build a query with a capped page size, execute it through safeCall, and verify the returned property IDs and count against a synthetic fixture. For a mutation, persist an idempotency key before dispatch and treat a timeout or 429 as a pause/reconcile condition rather than calling the function again. If client configuration, response schema, or write outcome is unverified, stop the workflow and surface a redacted error to the owner.

Resources

Next Steps

Apply patterns in appfolio-core-workflow-a.

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/jeremylongshore-tons-of-skills-marketplace-appfolio-sdk-b0c761/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

jeremylongshore-tons-of-skills-marketplace-appfolio-sdk-b0c761.ocm.jsonjson
{
  "ocm": "1",
  "id": "jeremylongshore-tons-of-skills-marketplace-appfolio-sdk-b0c761",
  "kind": "skill",
  "name": "appfolio-sdk-patterns",
  "description": "Apply production-ready patterns for AppFolio REST API integration. Trigger: \"appfolio patterns\".",
  "publisher": "jeremylongshore",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "coding"
    ],
    "tags": [
      "skill-md",
      "saas",
      "property-management",
      "appfolio",
      "real-estate",
      "skills-sh"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Apply production-ready patterns for AppFolio REST API integration. Trigger: \"appfolio patterns\"."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "skills.sh",
      "repository": "https://github.com/jeremylongshore/tons-of-skills-marketplace",
      "path": "plugins/saas-packs/appfolio-pack/skills/appfolio-sdk-patterns/SKILL.md",
      "ref": "HEAD",
      "url": "https://github.com/jeremylongshore/tons-of-skills-marketplace/blob/HEAD/plugins/saas-packs/appfolio-pack/skills/appfolio-sdk-patterns/SKILL.md",
      "key": "jeremylongshore/tons-of-skills-marketplace/plugins/saas-packs/appfolio-pack/skills/appfolio-sdk-patterns/SKILL.md"
    },
    "compatibility": "Designed for Claude Code",
    "allowed_tools": [
      "Read,",
      "Write,",
      "Edit,",
      "Bash(npm:*),",
      "Bash(curl:*),",
      "Grep"
    ],
    "license": "MIT"
  },
  "instructions": "# AppFolio SDK Patterns\n\n## Overview\n\nProduction-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.\n\n## Prerequisites\n\n- A provider-verified base URL, auth method, endpoint scope, and managed secret\n  injection path for the target portfolio.\n- Schema validation, request timeouts, endpoint",
  "cost": {
    "context_tokens": 1551
  }
}

Fetch it by URL: GET /api/v1/registry/jeremylongshore-tons-of-skills-marketplace-appfolio-sdk-b0c761/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.