Fync

Core Concepts

Understanding how Fync works under the hood

Core Concepts

Architecture Overview

Fync follows this structure:

Provider Factory → Module → Resources → Methods → API Calls
  • Provider Factory: Functions like GitHub(), Spotify()
  • Module: The configured API client
  • Resources: Groups of related endpoints (repos, users, playlists)
  • Methods: Individual API operations (getRepo, getUser, getPlaylist)

Modules & Resources

Every provider is a factory function that creates a module:

import { GitHub, Spotify } from '@remcostoeten/fync';

// Create modules
const github = GitHub({ token: process.env.GITHUB_TOKEN! });
const spotify = Spotify({ token: process.env.SPOTIFY_TOKEN! });

// Modules contain resources
console.log(github.repos);    // Repository resource
console.log(github.users);    // User resource
console.log(github.search);   // Search resource

console.log(spotify.tracks);    // Track resource
console.log(spotify.playlists); // Playlist resource
console.log(spotify.player);    // Player resource

Resource Structure

Each resource has:

  • basePath: Base URL path (e.g., /repos, /users/{username})
  • methods: Object of API methods with their HTTP verbs and paths

Path Interpolation

Path Parameters

API endpoints have placeholders in their paths. Fync fills these automatically:

// API endpoint: GET /repos/{owner}/{repo}
// Call:
const repo = await github.repos.getRepo({
  owner: 'facebook',  // Fills {owner}
  repo: 'react'       // Fills {repo}
});

// Result: GET https://api.github.com/repos/facebook/react

Query Parameters

Any extra properties in your options object become query parameters:

// API endpoint: GET /repos/{owner}/{repo}/commits
// Call:
const commits = await github.repos.getRepoCommits({
  owner: 'facebook',     // Path param
  repo: 'react',         // Path param
  per_page: 50,          // Query param ?per_page=50
  page: 2,               // Query param ?page=2
  sha: 'main'            // Query param ?sha=main
});

// Result: GET /repos/facebook/react/commits?per_page=50&page=2&sha=main

Method Signatures

Two Patterns

1. Methods with data (POST/PUT/PATCH)

// Pattern: (data, options)
method(dataObject, options)

// Example:
const newRepo = await github.repos.createRepo(
  { name: 'my-new-repo', private: true },  // data
  {}                                       // options
);

2. Methods without data (GET/DELETE)

// Pattern: (options)
method(options)

// Example:
const repo = await github.repos.getRepo({
  owner: 'facebook',  // Path parameter
  repo: 'react'       // Path parameter
});

DELETE with Body

Some APIs like Spotify support DELETE with a request body:

// DELETE with body (supported!)
await spotify.playlists.removeTracksFromPlaylist(
  { tracks: [{ uri: 'spotify:track:123' }] },  // data (body)
  { playlist_id: 'abc' }                        // options (path params)
);

Response Handling

Automatic Parsing

Fync handles different response types automatically:

// JSON responses (most common)
const user = await github.getUser('octocat');
console.log(user.name);        // Automatically parsed object

// Text responses
const readme = await github.repos.getRepoReadme({ owner: 'facebook', repo: 'react' });
console.log(typeof readme);    // string (text content)

// Empty responses (204 No Content)
const result = await github.activity.starRepo({}, { owner: 'facebook', repo: 'react' });
console.log(result);           // undefined

Error Handling

Fync throws on non-2xx HTTP status codes:

try {
  const user = await github.getUser('nonexistent-user-12345');
} catch (error) {
  console.error('API Error:', error.message);
  // Error includes status code, response text, etc.
}

HTTP Configuration

Headers & Content Types

Fync handles common content types:

// JSON (default)
await github.repos.createRepo({ name: 'my-repo' });

// Form data (manually set header)
await someApi.createResource(
  { name: 'test', file: 'content' },
  {},
  { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);

// FormData (manually create)
const formData = new FormData();
formData.append('file', fileContent);
await someApi.uploadFile(formData);

Authentication

Most providers use bearer token authentication:

// Automatic header setting
const github = GitHub({ token: 'ghp_xxxxxxxxxxxxxxxxxxxx' });
const spotify = Spotify({ token: 'BQD...access_token...' });
const calendar = GoogleCalendar({ token: 'ya29.access_token...' });

For custom auth or headers:

const customClient = buildApi({
  baseUrl: 'https://api.example.com',
  auth: { type: 'bearer', token: 'custom-token' },
  headers: {
    'User-Agent': 'MyApp/1.0',
    'Accept': 'application/vnd.api.v1+json'
  }
});

Helper Methods vs Low-Level Access

Convenient, opinionated methods for common operations:

// Helper methods - easy to use
const user = await github.getUser('octocat');
const repo = await github.getRepository('facebook', 'react');
const playlist = await spotify.getPlaylist('37i9dQZF1DXcBWIGoYBM5M');
const events = await calendar.getUpcomingEvents('primary', 10);

Low-Level Resource Access

Full control over API calls:

// Direct resource access - full control
const user = await github.users.getUser({ username: 'octocat' });
const repo = await github.repos.getRepo({ owner: 'facebook', repo: 'react' });
const playlist = await spotify.playlists.getPlaylist({ playlist_id: '37i9dQZF1DXcBWIGoYBM5M' });
const events = await calendar.events.listEvents({ calendarId: 'primary', maxResults: 10 });

Both approaches are valid and use the same underlying HTTP client.

Advanced Patterns

Request/Response Interception

For logging, debugging, or modifying requests:

// Note: This is a conceptual example
const client = createApiClient({
  interceptors: {
    request: (config) => {
      console.log('Making request:', config.method, config.url);
      return config;
    },
    response: (response) => {
      console.log('Got response:', response.status);
      return response;
    }
  }
});

Pagination

Many APIs return paginated results. Handle them like this:

// Get first page
const firstPage = await github.users.getUserRepos({
  username: 'octocat',
  per_page: 30,
  page: 1
});

// Get next page (if available)
if (firstPage.length === 30) {
  const secondPage = await github.users.getUserRepos({
    username: 'octocat',
    per_page: 30,
    page: 2
  });
}

Batching Requests

For APIs that support batching, structure your data appropriately:

// Example: Get multiple tracks at once
const tracks = await spotify.tracks.getTracks({
  ids: '4iV5W9uYEdYUVa79Axb7Rh,5U2Wd7q3gXUbPDjXEAP0BM'
});

Best Practices

1. Use Helper Methods When Available

They're simpler and handle common patterns for you.

2. Handle Errors Gracefully

Always wrap API calls in try/catch blocks.

3. Use Type Safety

TypeScript definitions are included - leverage them!

4. Cache When Appropriate

Some APIs have rate limits - cache responses when you can.

5. Use Environment Variables for Tokens

Never commit tokens to your repository:

const github = GitHub({
  token: process.env.GITHUB_TOKEN!
});

Debugging

Enable Request Logging

Most providers support debug modes or logging:

// Example: Debug mode (conceptual)
const github = GitHub({
  token: process.env.GITHUB_TOKEN!,
  debug: true  // Enable request/response logging
});

Common Issues

  1. 401 Unauthorized: Check your token is valid
  2. 403 Forbidden: Check token permissions/rate limits
  3. 404 Not Found: Verify path parameters are correct
  4. 422 Unprocessable Entity: Check request body format

Understanding these core concepts will help you use Fync effectively across all supported providers!