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:
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
- Backward compatibility: Version 1 endpoints will remain stable and backward-compatible
- Breaking changes: Major changes will be released as
v2,v3, etc. - Deprecation notice: At least 6 months advance notice before deprecating endpoints
- Parallel versions: Old and new versions will run simultaneously during deprecation period
Deprecation process
When an endpoint is deprecated:
- Announcement: Notification via email and in-app message
- Documentation: Clear migration guide published
- Headers: Deprecated endpoints return
X-API-Deprecated: trueheader - Sunset header:
Sunsetheader indicates removal date (RFC 8594) - 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
- Always use HTTPS in production
- Store API keys securely (environment variables, secret managers)
- Implement retry logic with exponential backoff
- Handle pagination properly for large datasets
- Validate data before sending to API
- Log errors for debugging
- Monitor API usage and set up alerts
- Keep API client libraries up to date
- Test thoroughly before production deployment
- Read response headers for rate limit info (when implemented)