API Documentation - jra3/mulm GitHub Wiki

API Documentation

This document describes the JSON API endpoints available in the Mulm application. All API routes are prefixed with /api/ and return JSON responses.

Overview

The Mulm API provides search endpoints for typeahead/autocomplete functionality. These endpoints are designed for low-latency, real-time search as users type.

Base URL: https://bap.basny.org/api/

Authentication: Most API endpoints require an active session (cookie-based authentication). Unauthenticated requests will receive appropriate error responses.

Rate Limiting: API endpoints are rate-limited to 30 requests/second per IP address (burst up to 50).

Response Format

All API endpoints return JSON. Successful responses return the requested data directly. Error responses follow a standard format:

{
  "error": "Human-readable error message",
  "code": "ERROR_CODE",
  "details": {}
}

Endpoints

Member Search

Search for members by name or email (typeahead/autocomplete).

GET /api/members/search

Query Parameters:

Parameter Type Required Description
q string Yes Search query (minimum 2 characters)

Example Request:

GET /api/members/search?q=john

Example Response:

[
  {
    "value": "John Smith",
    "text": "John Smith",
    "email": "[email protected]"
  },
  {
    "value": "Johnny Doe",
    "text": "Johnny Doe",
    "email": "[email protected]"
  }
]

Response Schema:

interface MemberTypeaheadItem {
  /** Member display name (used as form value) */
  value: string;
  /** Member display name (for display) */
  text: string;
  /** Member contact email */
  email: string;
}

type MemberSearchResponse = MemberTypeaheadItem[] | ApiErrorResponse;

Search Behavior:

  • Case-insensitive partial matching on display_name and contact_email
  • Searches from the beginning of words (e.g., "joh" matches "John" but not "Rajohn")
  • Minimum query length: 2 characters (returns empty array if shorter)
  • Maximum results: Limited by database query (typically 10-20 results)
  • Results ordered by relevance

Error Responses:

Status Code Error Description
500 Search failed Database error or unexpected failure

Usage:

  • Used in witness selection during submission creation
  • Used in admin interfaces for selecting members

Species Search

Search for species by common name or scientific name (typeahead/autocomplete).

GET /api/species/search

Query Parameters:

Parameter Type Required Description
q string Yes Search query (partial name match)
species_type string No Filter by species type (e.g., "fish", "plant", "coral")
species_class string No Filter by species class (e.g., "Catfish", "Livebearers")

Example Request:

GET /api/species/search?q=betta&species_type=fish

Example Response:

[
  {
    "text": "Betta (Betta splendens)",
    "common_name": "Betta",
    "scientific_name": "Betta splendens",
    "program_class": "Anabantoids",
    "group_id": 142,
    "name_id": 201
  },
  {
    "text": "Giant Betta (Betta anabatoides)",
    "common_name": "Giant Betta",
    "scientific_name": "Betta anabatoides",
    "program_class": "Anabantoids",
    "group_id": 143,
    "name_id": 202
  }
]

Response Schema:

interface SpeciesTypeaheadItem {
  /** Display text combining common and scientific names */
  text: string;
  /** Common name */
  common_name: string;
  /** Scientific name (genus + species) */
  scientific_name: string;
  /** BAP program classification */
  program_class: string;
  /** Species group ID (for grouping synonyms) */
  group_id: number;
  /** Specific name variant ID (foreign key for submissions) */
  name_id: number;
}

type SpeciesSearchResponse = SpeciesTypeaheadItem[] | ApiErrorResponse;

Search Behavior:

  • Case-insensitive partial matching on both common and scientific names
  • Searches across all name variants in species catalog
  • Maximum results: 10 (optimized for typeahead performance)
  • Results ordered by relevance (exact matches prioritized)
  • Optional filtering by species type and class

Species Types:

  • fish - Fish species
  • plant - Aquatic plants
  • coral - Corals and marine invertebrates

Species Classes (Examples):

  • Fish: Catfish, Livebearers, Cichlids, Killifish, Anabantoids
  • Plant: Various botanical classifications
  • Coral: Marine invertebrate classifications

Error Responses:

Status Code Error Description
400 Invalid query parameters Malformed species_type or species_class
500 Search failed Database error or unexpected failure

Usage:

  • Used in submission forms for species selection
  • Populates species name and scientific name fields
  • Links submission to canonical species catalog entry

Rate Limiting

API endpoints use nginx-based rate limiting:

Endpoint Pattern Limit Burst Description
/api/* 30 req/sec 50 All API endpoints

Rate Limit Headers: Rate limit information is not currently exposed in response headers, but may be added in future versions.

Rate Limit Exceeded: When rate limit is exceeded, nginx returns:

  • Status Code: 503 Service Unavailable
  • Body: nginx error page

Best Practices:

  • Debounce typeahead searches (recommended: 300ms delay)
  • Cache results client-side when possible
  • Avoid making requests for every keystroke

Error Handling

Standard Error Response

All API errors return JSON with this structure:

interface ApiErrorResponse {
  /** Human-readable error message */
  error: string;
  /** Optional error code for programmatic handling */
  code?: string;
  /** Optional additional details */
  details?: unknown;
}

Common Error Codes

Code HTTP Status Description
SEARCH_FAILED 500 Database query failed
INVALID_PARAMETERS 400 Query parameters are invalid
UNAUTHORIZED 401 Authentication required
FORBIDDEN 403 Insufficient permissions

Error Response Examples

Search Failed:

{
  "error": "Failed to search members. Please try again.",
  "code": "SEARCH_FAILED"
}

Invalid Parameters:

{
  "error": "Invalid species_type. Must be one of: fish, plant, coral",
  "code": "INVALID_PARAMETERS"
}

Authentication

API endpoints require cookie-based session authentication. Sessions are established through the web interface:

  1. User logs in via /auth/login or OAuth
  2. Session cookie is set (HTTP-only, secure in production)
  3. Session cookie is sent automatically with API requests
  4. Session is validated on each request

Session Expiration:

  • Sessions expire after a period of inactivity
  • Expired sessions return 401 Unauthorized
  • Frontend should redirect to login page

CSRF Protection: Currently not implemented for API endpoints (read-only operations).

Performance Considerations

Database Optimization

API endpoints are optimized for performance:

  • Indexed searches: All search fields have database indexes
    • members.contact_email - B-tree index
    • members.display_name - Search-optimized index
    • species_name.common_name + species_name.scientific_name - Composite index
  • Result limiting: Queries are limited at database level (not in application code)
  • Connection pooling: Separate read-only database connection for search queries

Response Times

Expected response times under normal load:

Endpoint P50 P95 P99
/api/members/search <50ms <100ms <200ms
/api/species/search <75ms <150ms <250ms

Factors affecting performance:

  • Database size (species catalog: ~1000+ entries)
  • Query complexity (filters add minimal overhead)
  • Server load and concurrent requests
  • Network latency

Caching

Current caching strategy:

  • Browser caching: Not enabled (search results may change)
  • Server-side caching: Not implemented (data changes infrequently, could be added)
  • CDN caching: Not applicable (dynamic API responses)

Future improvements:

  • Redis cache for species catalog (rarely changes)
  • ETag support for conditional requests
  • Response compression (gzip)

Integration Examples

Vanilla JavaScript (Fetch)

// Member search with debouncing
let searchTimeout;
const searchInput = document.getElementById('member-search');

searchInput.addEventListener('input', (e) => {
  clearTimeout(searchTimeout);
  const query = e.target.value;

  if (query.length < 2) return;

  searchTimeout = setTimeout(async () => {
    const response = await fetch(`/api/members/search?q=${encodeURIComponent(query)}`);
    const members = await response.json();

    if (Array.isArray(members)) {
      // Display results
      console.log('Found members:', members);
    } else {
      // Handle error
      console.error('Search error:', members.error);
    }
  }, 300);
});

Tom-Select (Current Implementation)

// Species typeahead using Tom-Select
new TomSelect('#species-search', {
  valueField: 'name_id',
  labelField: 'text',
  searchField: ['common_name', 'scientific_name'],
  load: function(query, callback) {
    if (query.length < 2) return callback();

    fetch(`/api/species/search?q=${encodeURIComponent(query)}`)
      .then(response => response.json())
      .then(data => {
        if (Array.isArray(data)) {
          callback(data);
        } else {
          callback();
          console.error('Species search error:', data.error);
        }
      })
      .catch(() => callback());
  }
});

Error Handling Pattern

async function searchMembers(query) {
  try {
    const response = await fetch(`/api/members/search?q=${encodeURIComponent(query)}`);

    // Check HTTP status
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const data = await response.json();

    // Check for API error response
    if ('error' in data) {
      console.error('API Error:', data.error);
      return [];
    }

    return data;
  } catch (error) {
    console.error('Network error:', error);
    return [];
  }
}

Future API Endpoints

Potential endpoints under consideration:

  • GET /api/submissions/:id - Get submission details
  • GET /api/standings/:program - Get program standings data
  • GET /api/member/:memberId/stats - Get member statistics
  • POST /api/submissions/:id/images - Upload submission images
  • DELETE /api/submissions/:id/images/:imageKey - Delete submission image

Related Documentation