A simple Express.js CRUD API demonstrating SolvaPay paywall protection.
- Features
- Quick Start
- Code Walkthrough
- API Endpoints
- Usage Examples
- Testing the Paywall
- Architecture
- Key Implementation Details
- Error Responses
- Best Practices
- Testing
- Troubleshooting
- Related Documentation
- Express.js REST API with full CRUD operations
- SolvaPay Paywall protection on all endpoints
- Demo Mode: Uses stub client - no backend required
- Auto Port Detection: Finds next available port automatically
- Usage Limits: Shared limit across protected routes for the configured product
- Clean Architecture: Separated business logic and route handlers
cd examples/express-basic
pnpm installNo configuration needed! Uses a stub client that simulates the backend:
pnpm devDemo Mode:
- No backend required
- 5 free calls per day
- Local usage tracking (resets daily)
GET /- API information and documentationGET /health- Health check
All require x-customer-ref header for user identification:
POST /tasks- Create a new taskGET /tasks- List all tasksGET /tasks/:id- Get a specific taskDELETE /tasks/:id- Delete a task
curl -X POST http://localhost:3001/tasks \
-H "Content-Type: application/json" \
-H "x-customer-ref: demo_user" \
-d '{"title": "My first task", "description": "Testing paywall"}'curl http://localhost:3001/tasks \
-H "x-customer-ref: demo_user"curl http://localhost:3001/tasks/task_1 \
-H "x-customer-ref: demo_user"curl -X DELETE http://localhost:3001/tasks/task_1 \
-H "x-customer-ref: demo_user"Make 6+ requests with the same x-customer-ref to trigger the paywall:
# These will succeed (1-5)
for i in {1..5}; do
curl -X POST http://localhost:3001/tasks \
-H "Content-Type: application/json" \
-H "x-customer-ref: demo_user" \
-d "{\"title\": \"Task $i\"}"
echo ""
done
# This will return 402 Payment Required (6th call)
curl -X POST http://localhost:3001/tasks \
-H "Content-Type: application/json" \
-H "x-customer-ref: demo_user" \
-d '{"title": "Task 6"}'| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
No | 3001 |
Server port |
┌─────────────────┐
│ Express App │
├─────────────────┤
│ Business Logic │
│ - createTask │
│ - getTask │
│ - listTasks │
│ - deleteTask │
└────────┬────────┘
│
▼
┌─────────────────┐
│ SolvaPay SDK │
│ payable.http() │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Stub Client │
│ (Demo Mode) │
└─────────────────┘
src/
├── index.ts # Main Express app with routes
└── __tests__/
└── api.test.ts # Integration tests
This section provides a detailed walkthrough of how the example is structured and how SolvaPay integrates with Express.js.
The example uses a stub client for local development, which simulates the SolvaPay backend without requiring API keys:
// src/index.ts
import { createSolvaPay } from '@solvapay/server'
import { createStubClient } from '../../shared/stub-api-client'
// Create stub client for demo (no backend required)
const apiClient = createStubClient({
freeTierLimit: 5, // 5 free calls per day
debug: true, // Enable debug logging
})
// Initialize SolvaPay with the stub client
const solvaPay = createSolvaPay({
apiClient,
})For Production: Replace the stub client with a real API client:
import { createSolvaPayClient } from '@solvapay/server'
const apiClient = createSolvaPayClient({
apiKey: process.env.SOLVAPAY_SECRET_KEY!,
baseUrl: process.env.SOLVAPAY_API_BASE_URL,
})
const solvaPay = createSolvaPay({ apiClient })Create a payable handler that will protect your endpoints:
// Create payable handler with product configuration
const payable = solvaPay.payable({
product: 'prd_NO8WYSX5', // Your product reference
})Note: The product reference should match what you've configured in your SolvaPay Console.
Wrap your business logic functions with the HTTP adapter:
// Import business logic functions
import { createTask, getTask, listTasks, deleteTask } from '@solvapay/demo-services'
// Protect endpoints using the HTTP adapter
app.post('/tasks', payable.http(createTask))
app.get('/tasks/:id', payable.http(getTask))
app.get('/tasks', payable.http(listTasks))
app.delete('/tasks/:id', payable.http(deleteTask))Your business logic functions receive parsed request data and authentication info:
// Example business logic function signature
async function createTask(args: {
title: string
description?: string
auth?: {
customer_ref?: string // Extracted from x-customer-ref header
}
}): Promise<{ success: boolean; task: Task }> {
// Your business logic here
const task = {
id: `task_${Date.now()}`,
title: args.title,
description: args.description,
createdAt: new Date().toISOString(),
}
return {
success: true,
task,
}
}Key Points:
- The
authobject contains customer identification extracted from headers - The function receives parsed JSON body and route parameters
- Return value is automatically formatted as JSON response
- Errors are automatically handled by the adapter
Here's what happens when a request comes in:
- Request arrives at Express route
- HTTP adapter intercepts the request
- Extracts customer reference from
x-customer-refheader - Checks limits via SolvaPay API (or stub client)
- If within limits: Execute business logic function
- If limit exceeded: Return 402 Payment Required with checkout URL
- Format response as JSON
The adapter automatically handles errors:
// PaywallError is automatically caught and formatted
// Returns 402 with checkout URL:
{
"success": false,
"error": "Payment required",
"product": "prd_NO8WYSX5",
"checkoutUrl": "https://checkout.solvapay.com/...",
"message": "Purchase required. Remaining: 0"
}
// Other errors are returned as 500 with error messageThe HTTP adapter (payable.http()) is a middleware function that:
- Parses request body and route parameters
- Extracts customer reference from headers
- Checks purchase limits
- Executes your business logic
- Formats the response
- Handles errors
The adapter extracts the customer reference from the x-customer-ref header:
curl -H "x-customer-ref: user_123" http://localhost:3001/tasksProduction Tip: In production, you'd typically extract this from:
- JWT tokens
- Session data
- Authentication middleware
Create a payable handler for your product and apply it to endpoints:
const payable = solvaPay.payable({ product: 'my-product' })
app.post('/tasks', payable.http(createTask))
app.get('/tasks', payable.http(listTasks))Your functions should:
- Accept parsed arguments (body + params + auth)
- Return a result object
- Handle business logic errors
- Be async (return Promises)
Returned when user exceeds their usage limits:
{
"success": false,
"error": "Payment required",
"product": "express-tasks-api",
"checkoutUrl": "https://checkout.solvapay.com/...",
"message": "Purchase required. Remaining: 0"
}Returned when a task doesn't exist:
{
"success": false,
"error": "Task not found"
}Returned for validation errors:
{
"success": false,
"error": "Title is required"
}Always use the stub client during development to avoid API rate limits and costs:
const apiClient = createStubClient({
freeTierLimit: 5,
debug: true,
})Keep your business logic separate from route handlers:
// Good: Business logic in separate function
app.post('/tasks', payable.http(createTask))
// Bad: Business logic in route handler
app.post(
'/tasks',
payable.http(async args => {
// Logic here makes testing harder
}),
)Always validate that customer reference is present:
async function createTask(args: { title: string; auth?: { customer_ref?: string } }) {
if (!args.auth?.customer_ref) {
throw new Error('Customer reference required')
}
// ... rest of logic
}Store configuration in environment variables:
const solvaPay = createSolvaPay({
apiClient:
process.env.USE_STUB_CLIENT === 'true'
? createStubClient({ freeTierLimit: 5 })
: createSolvaPayClient({
apiKey: process.env.SOLVAPAY_SECRET_KEY!,
baseUrl: process.env.SOLVAPAY_API_BASE_URL,
}),
})The adapter handles PaywallError automatically, but handle other errors:
async function createTask(args: CreateTaskArgs) {
try {
// Your logic
} catch (error) {
if (error instanceof ValidationError) {
throw error // Adapter will format as 400
}
throw error // Adapter will format as 500
}
}Use the stub client in tests for fast, reliable testing:
import { createStubClient } from '../../shared/stub-api-client'
const apiClient = createStubClient({ freeTierLimit: 5 })
const solvaPay = createSolvaPay({ apiClient })Problem: Port 3001 is already in use.
Solution: The server automatically finds the next available port:
Port 3001 is in use, trying 3002...
You can also set a custom port:
PORT=3005 pnpm devProblem: Errors like Cannot find module '@solvapay/server'.
Solution: Build the SDK packages from the workspace root:
# From workspace root
pnpm build:packagesThen restart the example server.
Problem: Making requests but paywall never triggers.
Solution:
- Check that you're using the same
x-customer-refheader value - Verify the stub client is configured correctly
- Check console logs for debug information
- Ensure you're making requests to protected endpoints
Problem: Getting 402 but no checkoutUrl in response.
Solution:
- Check that product references are correct
- Verify stub client configuration
- In production, ensure API key is valid
- Check network tab for API errors
Problem: Requests return errors before reaching business logic.
Solution:
- Check that
x-customer-refheader is present - Verify function signature matches expected format
- Check that function returns a Promise
- Review error logs for specific issues
Problem: Tests are failing with various errors.
Solution:
- Ensure stub client is used in tests
- Check that test data is cleaned up between runs
- Verify test environment variables are set
- Run tests with
--reporter=verbosefor more details
Problem: Type errors in business logic functions.
Solution:
- Ensure function signature matches
PayableFunctiontype - Check that return type is a Promise
- Verify argument types match what adapter provides
- Review TypeScript configuration
This example includes tests to verify the Express integration with SolvaPay.
# Run tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Run with coverage
pnpm test:coverageThe test suite (api.test.ts) verifies:
- Unprotected routes (health check, API info)
- Paywall protection on protected endpoints
- Free tier enforcement (5 operations per user)
- Blocking after limit exceeded
- Per-user usage isolation
- Consistent limit tracking across operations
- Error handling (missing customer_ref, etc.)
- Concurrent requests from different users
No setup required - tests use the stub client by default for fast, reliable testing.
[PASS] Express Basic API - Paywall Tests (8 tests)
[PASS] Unprotected Routes
[PASS] Paywall Free Tier
[PASS] Paywall Error Handling
[PASS] Paywall Isolation
For comprehensive backend integration tests of the SolvaPay SDK itself, see the Server SDK tests.
- Examples Overview - Overview of all examples
- Installation Guide - SDK installation
- Quick Start Guide - 5-minute Express setup
- Core Concepts - Understanding agents, plans, and paywalls
- Express.js Integration Guide - Complete Express integration guide
- Error Handling Guide - Error handling patterns
- Testing Guide - Testing with stub mode
- Server SDK API Reference - Complete API documentation
- Server SDK README - Package documentation
- SolvaPay Documentation - Official documentation
- Express.js Documentation - Express.js framework docs
- GitHub Repository - Source code and issues
See the root LICENSE file.