Server Contract
HTTP API specification for integrating honolytics with your backend.
What is the Server Contract?
The server contract defines the HTTP API that honolytics React hooks expect from your backend server. When you use HTTP mode (not storage mode), the hooks make requests to your server to fetch analytics data.
// HTTP mode - hooks call your server
<HonolyticsProvider
apiKey="your-api-key"
endpoint="https://your-api.com/analytics"
>
<App />
</HonolyticsProvider>
// Your hooks will call: GET https://your-api.com/analytics/metrics
const { data } = useAnalytics()Why Do You Need This?
When HTTP Mode Makes Sense
- Existing Analytics Backend: You already have analytics data in your database
- Cross-Device Tracking: Need to aggregate data across multiple devices/browsers
- Server-Side Processing: Want to do heavy analytics processing on the server
- Multi-User Applications: Analytics shared across different users
- Legacy Integration: Integrating with existing analytics infrastructure
Alternative: Storage Mode
If you don't want to build a server API, consider storage mode instead:
// Storage mode - no server needed
<HonolyticsProvider storageMode={true}>
<App />
</HonolyticsProvider>See Storage Engine for client-side analytics options.
The API Contract
Endpoint
Your server must implement a single endpoint:
GET {your-endpoint}/metricsRequest Format
Headers:
x-api-key: string— Required for authenticationContent-Type: application/json
Query Parameters (Optional):
start_date: string— ISO 8601 date string (e.g.,2024-01-01T00:00:00.000Z)end_date: string— ISO 8601 date string (e.g.,2024-01-31T23:59:59.999Z)
Example Request:
curl "https://your-api.com/analytics/metrics?start_date=2024-01-01T00:00:00.000Z&end_date=2024-01-31T23:59:59.999Z" \
-H "x-api-key: your-secret-key"Response Format
Your endpoint must return JSON with this exact structure:
{
"totals": {
"users": 1247,
"sessions": 1891,
"pageviews": 4552,
"avgDuration": 127.5
},
"timeseries": [
{ "date": "2024-01-01", "users": 45, "sessions": 67, "pageviews": 128 },
{ "date": "2024-01-02", "users": 52, "sessions": 74, "pageviews": 156 }
],
"breakdowns": {
"topPages": [
{ "url": "/dashboard", "views": 1205, "avgDuration": 245.3 },
{ "url": "/profile", "views": 892, "avgDuration": 156.7 }
],
"countries": [
{ "country": "United States", "users": 445 },
{ "country": "Germany", "users": 198 }
],
"browsers": [
{ "browser": "Chrome", "users": 756 },
{ "browser": "Firefox", "users": 234 }
],
"devices": [
{ "device": "Desktop", "users": 687 },
{ "device": "Mobile", "users": 423 }
]
}
}Implementation Examples
Minimal Hono.js Server
import { Hono } from 'hono'
const app = new Hono()
app.get('/metrics', async (c) => {
// 1. Authenticate the request
const apiKey = c.req.header('x-api-key')
if (!apiKey || apiKey !== process.env.ANALYTICS_API_KEY) {
return c.json({ error: 'Unauthorized' }, 401)
}
// 2. Parse date range (optional)
const startDate = c.req.query('start_date')
const endDate = c.req.query('end_date')
// 3. Query your database (replace with your logic)
const analytics = await getAnalyticsFromDatabase({
start: startDate ? new Date(startDate) : undefined,
end: endDate ? new Date(endDate) : undefined
})
// 4. Return the required format
return c.json(analytics)
})
export default appNext.js API Route
// app/api/analytics/metrics/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const apiKey = request.headers.get('x-api-key')
if (!apiKey || apiKey !== process.env.ANALYTICS_API_KEY) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const startDate = searchParams.get('start_date')
const endDate = searchParams.get('end_date')
// Your analytics logic here
const data = await fetchAnalytics({ startDate, endDate })
return Response.json(data)
}Express.js Server
import express from 'express'
const app = express()
app.get('/metrics', async (req, res) => {
const apiKey = req.headers['x-api-key']
if (!apiKey || apiKey !== process.env.ANALYTICS_API_KEY) {
return res.status(401).json({ error: 'Unauthorized' })
}
const { start_date, end_date } = req.query
// Your database query logic
const analytics = await queryAnalytics(start_date, end_date)
res.json(analytics)
})Data Requirements
Field Specifications
Totals:
users: number— Unique user countsessions: number— Unique session countpageviews: number— Total pageview countavgDuration: number— Average session duration in seconds
Timeseries:
date: string— Date inYYYY-MM-DDformatusers: number— Daily unique userssessions: number— Daily unique sessionspageviews: number— Daily pageviews
Breakdowns:
- topPages:
url(string),views(number),avgDuration(seconds) - countries:
country(string),users(number) - browsers:
browser(string),users(number) - devices:
device(string),users(number)
Empty Data Handling
// Return empty arrays/zeros when no data exists
{
"totals": { "users": 0, "sessions": 0, "pageviews": 0, "avgDuration": 0 },
"timeseries": [],
"breakdowns": {
"topPages": [],
"countries": [],
"browsers": [],
"devices": []
}
}Security Considerations
API Key Authentication
// ✅ Good: Secure API key validation
const apiKey = request.headers.get('x-api-key')
if (!apiKey || !secureCompare(apiKey, process.env.ANALYTICS_API_KEY)) {
return unauthorized()
}
// ❌ Bad: Timing attack vulnerable
if (apiKey !== process.env.ANALYTICS_API_KEY) {
return unauthorized()
}CORS Configuration
// Configure CORS for your frontend domain
app.use(cors({
origin: ['https://your-app.com', 'https://staging.your-app.com'],
credentials: true
}))Rate Limiting
// Protect against abuse
app.use('/metrics', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
}))Testing Your Implementation
Manual Testing
# Test authentication
curl "http://localhost:3000/metrics" \
-H "x-api-key: wrong-key"
# Should return 401
# Test valid request
curl "http://localhost:3000/metrics" \
-H "x-api-key: correct-key"
# Should return analytics data
# Test with date range
curl "http://localhost:3000/metrics?start_date=2024-01-01T00:00:00.000Z&end_date=2024-01-31T23:59:59.999Z" \
-H "x-api-key: correct-key"Integration Testing
// Test with actual React hooks
function TestComponent() {
const { data, loading, error } = useAnalytics()
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!data) return <div>No data</div>
return (
<div>
<p>Users: {data.totals.users}</p>
<p>Sessions: {data.totals.sessions}</p>
<p>Pageviews: {data.totals.pageviews}</p>
</div>
)
}Common Issues
CORS Errors
Access to fetch at 'https://api.example.com/metrics' from origin 'https://app.example.com' has been blocked by CORS policySolution: Configure CORS headers on your server.
Authentication Failures
401 UnauthorizedSolution: Check that x-api-key header matches your server's expected key.
Invalid Response Format
Error: Expected object with 'totals' propertySolution: Ensure your response matches the exact JSON structure above.
Next Steps
For comprehensive examples and advanced patterns:
- Database Integration Examples — PostgreSQL, MySQL, SQLite examples
- Framework Examples — Next.js, Remix, SvelteKit implementations
- Authentication Patterns — JWT, session-based, API key strategies
- Performance Optimization — Caching, indexing, query optimization
Storage Mode Alternative
If implementing a server API seems complex, consider using Storage Mode instead:
- No Server Required: Analytics run entirely client-side
- Multiple Storage Options: localStorage, IndexedDB, PostgreSQL, Turso
- Same React Hooks: Identical API, just change the provider mode