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_nameandcontact_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 speciesplant- Aquatic plantscoral- 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:
- User logs in via
/auth/loginor OAuth - Session cookie is set (HTTP-only, secure in production)
- Session cookie is sent automatically with API requests
- 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 indexmembers.display_name- Search-optimized indexspecies_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 detailsGET /api/standings/:program- Get program standings dataGET /api/member/:memberId/stats- Get member statisticsPOST /api/submissions/:id/images- Upload submission imagesDELETE /api/submissions/:id/images/:imageKey- Delete submission image
Related Documentation
- Database Schema - Database structure and relationships
- Production Deployment - Nginx configuration and rate limiting
- Security Overview - Authentication and security measures
- CLAUDE.md - Full project documentation