Implementation & versioning

Stack notes, testing guide, and API versioning strategy.

Stack overview

The My Prop Journal API is built on a modern, production-ready technology stack:

  • Application Framework: Next.js 15 (App Router)
  • Database: PostgreSQL with row-level security
  • Authentication: Clerk for user authentication; API keys for programmatic access
  • Validation: Zod schemas for request bodies and query parameters
  • Security: bcrypt hashing for API keys, database-level data isolation

Database schema

The API exposes data from several core tables:

TablePurpose
api_keysHashed API keys with metadata
trade_groupsMain trade records
executionsIndividual buy/sell fills
playbooksTrading strategy collections
strategiesIndividual strategies within playbooks
accountsProp firm and retail broker accounts
subscriptionsUser billing tiers and subscription status

Quick integration examples

Node.js / JavaScript

const API_KEY = process.env.MPJ_API_KEY;
const BASE_URL = 'https://app.mypropjournal.com/api/v1';

// Helper function for API requests
async function apiRequest(endpoint, options = {}) {
  const url = `${BASE_URL}${endpoint}`;
  const config = {
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
      ...options.headers
    },
    ...options
  };
  
  const response = await fetch(url, config);
  
  if (!response.ok) {
    const error = await response.json();
    throw new Error(`API Error: ${error.error}`);
  }
  
  return response.json();
}

// Fetch trades
async function getTrades(startDate, endDate) {
  const params = new URLSearchParams();
  if (startDate) params.append('start_date', startDate);
  if (endDate) params.append('end_date', endDate);
  
  return apiRequest(`/trades?${params}`);
}

// Create trade
async function createTrade(tradeData) {
  return apiRequest('/trades', {
    method: 'POST',
    body: JSON.stringify(tradeData)
  });
}

// Usage
const trades = await getTrades('2024-01-01', '2024-12-31');
console.log(`Found ${trades.pagination.total} trades`);

Python

import os
import requests
from typing import Dict, Any, Optional

API_KEY = os.environ['MPJ_API_KEY']
BASE_URL = 'https://app.mypropjournal.com/api/v1'

class MyPropJournalAPI:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = BASE_URL
        self.session = requests.Session()
        self.session.headers.update({
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json'
        })
    
    def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
        """Make API request with error handling"""
        url = f'{self.base_url}{endpoint}'
        response = self.session.request(method, url, **kwargs)
        response.raise_for_status()
        return response.json()
    
    def get_trades(self, start_date: Optional[str] = None, 
                   end_date: Optional[str] = None, 
                   page: int = 1, 
                   limit: int = 50) -> Dict[str, Any]:
        """Fetch trades with optional date filtering"""
        params = {'page': page, 'limit': limit}
        if start_date:
            params['start_date'] = start_date
        if end_date:
            params['end_date'] = end_date
        
        return self._request('GET', '/trades', params=params)
    
    def create_trade(self, trade_data: Dict[str, Any]) -> Dict[str, Any]:
        """Create a new trade"""
        return self._request('POST', '/trades', json=trade_data)
    
    def get_performance(self, start_date: Optional[str] = None,
                       end_date: Optional[str] = None) -> Dict[str, Any]:
        """Get performance overview"""
        params = {}
        if start_date:
            params['start_date'] = start_date
        if end_date:
            params['end_date'] = end_date
        
        return self._request('GET', '/performance/overview', params=params)

# Usage
api = MyPropJournalAPI(API_KEY)

# Get recent trades
trades = api.get_trades(limit=10)
print(f"Total trades: {trades['pagination']['total']}")

# Get performance
performance = api.get_performance(start_date='2024-01-01')
print(f"Win rate: {performance['data']['win_rate']:.2f}%")

Ruby

require 'net/http'
require 'json'
require 'uri'

class MyPropJournalAPI
  API_KEY = ENV['MPJ_API_KEY']
  BASE_URL = 'https://app.mypropjournal.com/api/v1'
  
  def initialize
    @headers = {
      'Authorization' => "Bearer #{API_KEY}",
      'Content-Type' => 'application/json'
    }
  end
  
  def get_trades(start_date: nil, end_date: nil, page: 1, limit: 50)
    params = { page: page, limit: limit }
    params[:start_date] = start_date if start_date
    params[:end_date] = end_date if end_date
    
    get('/trades', params)
  end
  
  def create_trade(trade_data)
    post('/trades', trade_data)
  end
  
  private
  
  def get(endpoint, params = {})
    uri = URI("#{BASE_URL}#{endpoint}")
    uri.query = URI.encode_www_form(params) unless params.empty?
    
    request = Net::HTTP::Get.new(uri)
    @headers.each { |key, value| request[key] = value }
    
    make_request(uri, request)
  end
  
  def post(endpoint, data)
    uri = URI("#{BASE_URL}#{endpoint}")
    request = Net::HTTP::Post.new(uri)
    @headers.each { |key, value| request[key] = value }
    request.body = data.to_json
    
    make_request(uri, request)
  end
  
  def make_request(uri, request)
    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
      http.request(request)
    end
    
    raise "API Error: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
    
    JSON.parse(response.body)
  end
end

# Usage
api = MyPropJournalAPI.new
trades = api.get_trades(limit: 10)
puts "Total trades: #{trades['pagination']['total']}"

Rate limiting

Current status

Rate limits are not currently enforced. The API will accept unlimited requests.

Future implementation

When rate limiting is added:

  • Standard tier: 1,000 requests per hour
  • Response code: 429 Too Many Requests
  • Headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After

Example rate limit response:

{
  "error": "Rate limit exceeded",
  "code": "RATE_LIMIT_EXCEEDED",
  "retry_after": 3600
}

Handling rate limits:

async function fetchWithRateLimit(url, options) {
  const response = await fetch(url, options);
  
  if (response.status === 429) {
    const retryAfter = response.headers.get('Retry-After');
    const waitTime = parseInt(retryAfter) * 1000;
    
    console.log(`Rate limited. Waiting ${retryAfter}s...`);
    await new Promise(resolve => setTimeout(resolve, waitTime));
    
    // Retry the request
    return fetchWithRateLimit(url, options);
  }
  
  return response;
}

API versioning

Current version

  • Version: v1 (stable)
  • Prefix: /api/v1
  • Status: Production-ready

Versioning strategy

  1. Backward compatibility: Version 1 endpoints will remain stable and backward-compatible
  2. Breaking changes: Major changes will be released as v2, v3, etc.
  3. Deprecation notice: At least 6 months advance notice before deprecating endpoints
  4. Parallel versions: Old and new versions will run simultaneously during deprecation period

Deprecation process

When an endpoint is deprecated:

  1. Announcement: Notification via email and in-app message
  2. Documentation: Clear migration guide published
  3. Headers: Deprecated endpoints return X-API-Deprecated: true header
  4. Sunset header: Sunset header indicates removal date (RFC 8594)
  5. Grace period: Minimum 6 months before endpoint removal

Example deprecated response:

HTTP/1.1 200 OK
X-API-Deprecated: true
Sunset: Sat, 1 Jan 2025 00:00:00 GMT
X-API-Deprecation-Info: https://docs.mypropjournal.com/api/migration-v2

{
  "data": [...],
  "deprecation_notice": "This endpoint will be removed on 2025-01-01. Please migrate to /api/v2/trades"
}

Testing guide

Authentication tests

Test API key authentication thoroughly:

// Test cases
const tests = [
  {
    name: 'Valid API key',
    key: 'mpj_valid_key',
    expectedStatus: 200
  },
  {
    name: 'Invalid API key',
    key: 'mpj_invalid_key',
    expectedStatus: 401
  },
  {
    name: 'Missing API key',
    key: null,
    expectedStatus: 401
  },
  {
    name: 'Malformed API key',
    key: 'invalid_format',
    expectedStatus: 401
  },
  {
    name: 'Revoked API key',
    key: 'mpj_revoked_key',
    expectedStatus: 401
  },
  {
    name: 'Free tier user',
    key: 'mpj_free_tier_key',
    expectedStatus: 403
  }
];

async function runAuthTests() {
  for (const test of tests) {
    const headers = test.key ? {
      'Authorization': `Bearer ${test.key}`,
      'Content-Type': 'application/json'
    } : {};
    
    const response = await fetch(
      'https://app.mypropjournal.com/api/v1/trades',
      { headers }
    );
    
    const passed = response.status === test.expectedStatus;
    console.log(`${passed ? '✓' : '✗'} ${test.name}: ${response.status}`);
  }
}

Data isolation tests

Verify that users can only access their own data:

def test_data_isolation():
    """Verify cross-user data access is blocked"""
    # Setup: Two users with separate API keys
    user_a_key = 'mpj_user_a_key'
    user_b_key = 'mpj_user_b_key'
    
    # User A creates a trade
    api_a = MyPropJournalAPI(user_a_key)
    trade = api_a.create_trade({
        'account_id': 'user-a-account-id',
        'symbol': 'TSLA',
        'side': 'long',
        'entry_date': '2024-01-15',
        'entry_price': 245.00
    })
    
    trade_id = trade['data']['id']
    print(f"User A created trade: {trade_id}")
    
    # User B tries to access User A's trade
    api_b = MyPropJournalAPI(user_b_key)
    
    try:
        api_b._request('GET', f'/trades/{trade_id}')
        print("✗ FAIL: User B accessed User A's trade")
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 404:
            print("✓ PASS: User B cannot access User A's trade")
        else:
            print(f"✗ FAIL: Unexpected error: {e}")
    
    # User B's list should not include User A's trades
    user_b_trades = api_b.get_trades()
    user_b_ids = [t['id'] for t in user_b_trades['data']]
    
    if trade_id not in user_b_ids:
        print("✓ PASS: User A's trade not in User B's list")
    else:
        print("✗ FAIL: User A's trade appears in User B's list")

test_data_isolation()

CRUD operation tests

Test all CRUD operations for each resource:

async function testTradeCRUD(apiKey) {
  const BASE_URL = 'https://app.mypropjournal.com/api/v1';
  const headers = {
    'Authorization': `Bearer ${apiKey}`,
    'Content-Type': 'application/json'
  };
  
  // CREATE
  console.log('Testing CREATE...');
  const createResponse = await fetch(`${BASE_URL}/trades`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      account_id: 'test-account-id',
      symbol: 'TEST',
      side: 'long',
      entry_date: '2024-01-15',
      entry_price: 100.00,
      quantity: 10
    })
  });
  
  const created = await createResponse.json();
  const tradeId = created.data.id;
  console.log(`✓ Created trade: ${tradeId}`);
  
  // READ (single)
  console.log('Testing READ (single)...');
  const readResponse = await fetch(`${BASE_URL}/trades/${tradeId}`, { headers });
  const trade = await readResponse.json();
  console.log(`✓ Retrieved trade: ${trade.data.symbol}`);
  
  // READ (list)
  console.log('Testing READ (list)...');
  const listResponse = await fetch(`${BASE_URL}/trades?limit=10`, { headers });
  const list = await listResponse.json();
  console.log(`✓ Retrieved ${list.data.length} trades`);
  
  // UPDATE
  console.log('Testing UPDATE...');
  const updateResponse = await fetch(`${BASE_URL}/trades/${tradeId}`, {
    method: 'PUT',
    headers,
    body: JSON.stringify({
      exit_price: 105.00,
      exit_date: '2024-01-16'
    })
  });
  
  const updated = await updateResponse.json();
  console.log(`✓ Updated trade exit price: ${updated.data.exit_price}`);
  
  // DELETE
  console.log('Testing DELETE...');
  const deleteResponse = await fetch(`${BASE_URL}/trades/${tradeId}`, {
    method: 'DELETE',
    headers
  });
  
  console.log(`✓ Deleted trade: ${tradeId}`);
  
  console.log('\n✓ All CRUD tests passed!');
}

Edge case tests

Test boundary conditions and error handling:

def test_edge_cases():
    """Test API edge cases and boundary conditions"""
    api = MyPropJournalAPI(API_KEY)
    
    # Test 1: Limit above maximum (should cap at 100)
    print("Testing limit > 100...")
    response = api.get_trades(limit=200)
    actual_limit = len(response['data'])
    assert actual_limit <= 100, f"Limit not capped: {actual_limit}"
    print(f"✓ Limit capped at {actual_limit}")
    
    # Test 2: Invalid date format
    print("Testing invalid date format...")
    try:
        api.get_trades(start_date='2024/01/15')  # Wrong format
        print("✗ Invalid date not rejected")
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 400:
            print("✓ Invalid date rejected with 400")
        else:
            print(f"✗ Wrong status code: {e.response.status_code}")
    
    # Test 3: Missing required fields
    print("Testing missing required fields...")
    try:
        api.create_trade({
            'symbol': 'TEST'
            # Missing required fields
        })
        print("✗ Incomplete trade data not rejected")
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 400:
            print("✓ Incomplete data rejected with 400")
        else:
            print(f"✗ Wrong status code: {e.response.status_code}")
    
    # Test 4: Invalid UUID format
    print("Testing invalid UUID...")
    try:
        api._request('GET', '/trades/invalid-uuid-format')
        print("✗ Invalid UUID not rejected")
    except requests.exceptions.HTTPError as e:
        if e.response.status_code in [400, 404]:
            print("✓ Invalid UUID rejected")
        else:
            print(f"✗ Wrong status code: {e.response.status_code}")
    
    print("\n✓ All edge case tests passed!")

test_edge_cases()

Best practices

  1. Always use HTTPS in production
  2. Store API keys securely (environment variables, secret managers)
  3. Implement retry logic with exponential backoff
  4. Handle pagination properly for large datasets
  5. Validate data before sending to API
  6. Log errors for debugging
  7. Monitor API usage and set up alerts
  8. Keep API client libraries up to date
  9. Test thoroughly before production deployment
  10. Read response headers for rate limit info (when implemented)