README
API Recorder - Local HTTP Recording & Playback Tool
A simple, professional Go application for recording and replaying HTTP interactions to enable offline development and testing during API migrations. Perfect for AI-assisted porting of services.
🎯 Purpose
This tool solves a specific problem: migrating from CRM v1 (Groovy) to CRM v2 (Go) without constant access to external APIs.
The AI Migration Workflow
Key Insight: With recorded HTTP interactions, AI can perfectly replicate API behavior without ever accessing the original external services.
🚀 Quick Start
# 1. Clone the repository
git clone https://github.com/jonathanleahy/proxyy.git
cd proxyy
# 2. Run the recorder in record mode
MODE=record go run main.go
# 3. Configure your CRM to use the proxy (see below)
# 4. Make some API calls to record them
# 5. Switch to playback mode for offline testing
MODE=playback go run main.go
📋 Features
- Zero Code Changes - Integration through configuration only
- Complete Header Preservation - All headers recorded and matched
- Simple URL-Based Proxy - Just prefix your API URLs
- Offline Development - Work without external dependencies
- Perfect for AI Migration - Provides ground truth for service behavior
🏗️ Architecture
Recording Mode:
Your App → localhost:8080/proxy?url=https://api.com → External API
↓ (saves request/response)
./recordings/
Playback Mode:
Your App → localhost:8080/proxy?url=https://api.com
↓ (returns recorded response)
./recordings/
🔧 Configuration
CRM v1 (Groovy) - application.yml
# Original
external-apis:
customer:
base-url: https://api.customer.com
# With recorder - just add proxy prefix
external-apis:
customer:
base-url: http://localhost:8080/proxy?url=https://api.customer.com
CRM v2 (Go) - .env
# Original
CUSTOMER_API=https://api.customer.com
# With recorder
CUSTOMER_API=http://localhost:8080/proxy?url=https://api.customer.comThat's it! No code changes needed.
📝 How It Works
Recording Structure
Each recording captures:- HTTP method, URL, headers, and body
- Complete response with all headers
- Request/response timing
- Service identification
{
"request": {
"method": "GET",
"url": "https://api.customer.com/customers/123",
"headers": {
"Authorization": ["Bearer token123"],
"Content-Type": ["application/json"]
},
"body": ""
},
"response": {
"status_code": 200,
"headers": {
"Content-Type": ["application/json"]
},
"body": "{\"id\":\"123\",\"name\":\"John Doe\"}"
}
}
Matching Logic
Requests are matched using a SHA256 hash of:- HTTP Method
- URL
- Headers (excluding volatile headers like Date)
- Request body
🧪 Testing Approach
TDD Unit Tests
func TestGenerateHash_SameInputProducesSameHash(t *testing.T) {
recorder := NewRecorder(Config{})
headers := map[string][]string{
"Authorization": {"Bearer token"},
"Content-Type": {"application/json"},
}
hash1 := recorder.generateHash("GET", "/api/test", headers, []byte("body"))
hash2 := recorder.generateHash("GET", "/api/test", headers, []byte("body"))
assert.Equal(t, hash1, hash2)
}
func TestServeRecordedResponse_ReturnsCorrectResponse(t *testing.T) {
recorder := NewRecorder(Config{Mode: "playback"})
// Add a recording
interaction := &RecordedInteraction{
Request: RequestData{
Method: "GET",
URL: "https://api.test.com/data",
},
Response: ResponseData{
StatusCode: 200,
Body: `{"status":"ok"}`,
},
}
hash := "testhash"
recorder.recordings[hash] = interaction
// Test serving it
w := httptest.NewRecorder()
recorder.serveRecordedResponse(w, hash)
assert.Equal(t, 200, w.Code)
assert.JSONEq(t, `{"status":"ok"}`, w.Body.String())
}
BDD Feature Tests
Feature: HTTP Request Recording
As a developer
I want to record HTTP requests and responses
So that I can replay them offline
Scenario: Recording a GET request
Given the recorder is in "record" mode
When I make a GET request to "/proxy?url=https://api.example.com/users/1"
Then the request should be forwarded to the external API
And the response should be saved to disk
And the client should receive the original response
Scenario: Replaying a recorded request
Given the recorder is in "playback" mode
And I have a recorded response for GET "/users/1"
When I make the same request
Then I should receive the recorded response
And no external request should be made
🛠️ API Endpoints
Proxy Endpoint
GET/POST/PUT/DELETE http://localhost:8080/proxy?url={targetUrl}
Admin Endpoints
GET http://localhost:8081/api/mode # Get current mode
POST http://localhost:8081/api/mode # Set mode {"mode": "record|playback"}
GET http://localhost:8081/api/recordings # List all recordings
GET http://localhost:8081/api/recording/{hash} # Get specific recording
📂 Project Structure
proxyy/
├── README.md # This file
├── main.go # Complete recorder implementation
├── main_test.go # Unit tests
├── features/ # BDD tests
│ ├── recording.feature
│ └── playback.feature
├── recordings/ # Stored recordings (auto-created)
├── scripts/
│ ├── start-record.sh
│ └── start-playback.sh
└── doc/
└── api-migration-project.md # Test project demonstrating usage
💡 AI-Assisted Migration Example
The power of this approach is that AI can create a functionally identical service in a different language using only:
The AI never needs access to the actual external APIs! It learns the expected behavior from the recordings and can generate code that behaves identically.
See doc/api-migration-project.md for a complete example with:
- Mock external services
- CRM v1 implementation (Node.js)
- CRM v2 implementation (Go)
- Test scripts to validate the migration
🔍 Environment Variables
MODE=record|playback # Operating mode (default: record)
STORAGE_PATH=./recordings # Where to store recordings (default: ./recordings)
PROXY_PORT=:8080 # Proxy port (default: :8080)
ADMIN_PORT=:8081 # Admin API port (default: :8081)
DEBUG=true|false # Enable debug logging (default: false)
📚 Complete Implementation
The full implementation is in main.go. Here are the key components:
Core Types
type RecordedInteraction struct {
Request RequestData `json:"request"`
Response ResponseData `json:"response"`
Metadata Metadata `json:"metadata"`
}
type Recorder struct {
config Config
recordings map[string]*RecordedInteraction
mu sync.RWMutex
}
Main Functions
generateHash()- Creates deterministic hash for request matchingProxyHandler()- Routes requests to record or playbackrecordAndForward()- Records while proxying to external APIserveRecordedResponse()- Returns recorded response in playback modesaveRecording()- Persists to diskloadRecording()- Loads from disk
🧑💻 Development Workflow
# Start recorder
MODE=record go run main.go
# Run your integration tests or manual testing
# All HTTP traffic through the proxy is recorded
# Switch to playback
MODE=playback go run main.go
# Run the same tests - now completely offline
# Responses come from recordings
# Test that v2 behaves identically to v1
# Using the same recorded responses
🤝 Contributing
This is a simple, focused tool. Contributions that maintain simplicity while adding value are welcome:
📄 License
MIT License - See LICENSE file for details
🔗 Related
- API Migration Test Project - Complete example using this recorder
- Go HTTP Package - Standard library HTTP
- Testify - Testing assertions
- Ginkgo - BDD testing framework
Remember: The goal is simple, professional, testable code that solves a real problem - enabling offline API migration testing and AI-assisted service porting.