Express 5 REST API for EquipChain — a decentralized utility meter monitoring and data access platform powered by Soroban smart contracts on Stellar.
- Project Overview
- Quick Start
- API Reference
- Architecture Overview
- Configuration Reference
- Development Guide
- Load Testing with k6
- Deployment Guide
- Contributing
- Related
- License
EquipChain is a blockchain-powered platform for monitoring, managing, and analyzing utility meter data. This backend service provides:
- REST API — Query meter status, contract data, and project information via a clean JSON API.
- Dashboard Analytics — Data aggregation endpoints for daily, monthly, custom-range summaries and fleet-wide metrics.
- Soroban Integration — Reads on-chain data from Stellar Soroban smart contracts for transparent, immutable meter records.
- Real-time Capabilities — Built on Express 5 with WebSocket support for live meter updates.
- Redis Caching — High-performance data caching for frequently accessed meter readings.
- Docker Support — Containerized deployment for consistent environments.
| Technology | Purpose |
|---|---|
| Express 5 | HTTP framework |
| Node.js 20+ | Runtime |
| Soroban (Stellar) | Smart contract integration |
| Redis | Caching layer |
| WebSockets | Real-time meter data |
| Docker | Containerization |
Current Phase: MVP — Core meter monitoring endpoints are operational. The API serves project metadata and on-chain contract data, and provides dashboard analytics aggregation. Future releases will add full CRUD for meters, WebSocket event streaming, and admin management.
git clone https://github.com/EquipChain/EquipChain-backend.git
cd EquipChain-backendnpm installCopy the example environment file and adjust as needed:
cp .env.example .env| Variable | Default | Required | Description |
|---|---|---|---|
PORT |
3000 |
No | Server listen port |
CONTRACT_ID |
CB7PSJZALNWNX7NLOAM6LOEL4OJZMFPQZJMIYO522ZSACYWXTZIDEDSS |
No | Stellar Soroban contract ID |
REDIS_URL |
— | No | Redis connection string for caching |
WS_ENABLED |
false |
No | Enable WebSocket support |
NODE_ENV |
development |
No | Environment mode |
LOG_LEVEL |
info |
No | Logging verbosity |
npm startThe server starts at http://localhost:3000.
curl http://localhost:3000
# {"project":"Equipchain","status":"Monitoring Meters","contract":"CB7PSJZALNWNX7NLOAM6LOEL4OJZMFPQZJMIYO522ZSACYWXTZIDEDSS"}Note: On first startup in development mode, the server automatically seeds ~6,480 sample meter readings across 3 meters spanning 90 days. This provides test data for the analytics endpoints immediately. Set
SKIP_SEED=1to disable auto-seeding.
All endpoints return JSON. Base URL: http://localhost:3000 (development) or your deployed URL.
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/ |
Project metadata (name, status, contract ID) | No |
GET |
/api/health |
Health check (uptime, status, timestamp) | No |
| Method | Path | Description | Auth |
|---|---|---|---|
POST |
/api/auth/challenge |
Mock wallet-based auth challenge (returns JWT) | No |
GET |
/api/protected |
Protected route requiring Bearer token | Yes |
POST |
/auth/login |
Authenticate and receive a JWT | No |
POST |
/auth/register |
Create a new user account | No |
POST |
/auth/refresh |
Refresh an expired token | Yes |
POST |
/auth/logout |
Invalidate current session | Yes |
Dashboard analytics and data aggregation endpoints for meter readings. Computed in-memory from stored readings.
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/api/analytics/daily-summary |
Daily aggregated readings within a date range | No |
GET |
/api/analytics/monthly-summary |
Monthly aggregated readings within a date range | No |
GET |
/api/analytics/custom-range |
Aggregated readings with configurable granularity | No |
GET |
/api/analytics/fleet-summary |
Fleet-wide summary across all meters | No |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
startDate |
string (ISO 8601) | Required | Start date (e.g., 2026-01-01) |
endDate |
string (ISO 8601) | Required | End date (e.g., 2026-01-31) |
meterIds |
string or string[] | All meters | Filter by one or more meter IDs |
timezone |
string (IANA) | UTC |
Timezone for day boundaries |
aggregationType |
enum | avg |
count, sum, avg, min, max, p50, p95 |
compareWith |
enum | — | previous_period, year_over_year |
Example Response:
GET /api/analytics/daily-summary?startDate=2026-01-01&endDate=2026-01-03&aggregationType=sum
{
"data": [
{ "key": "2026-01-01", "value": 350, "count": 3 },
{ "key": "2026-01-02", "value": 225, "count": 2 },
{ "key": "2026-01-03", "value": 425, "count": 2 }
],
"meta": {
"startDate": "2026-01-01",
"endDate": "2026-01-03",
"granularity": "day",
"aggregationType": "sum",
"timezone": "UTC",
"totalReadings": 7
}
}Same parameters as daily-summary, returns monthly rollups.
Additional Parameter: granularity — hour, day, week, month
Example Response:
{
"fleet": {
"totalReadings": 6480,
"totalMeters": 3,
"value": 112.5,
"aggregationType": "avg"
},
"meters": [
{ "meterId": "METER-001", "value": 150.2, "readings": 2160 },
{ "meterId": "METER-002", "value": 85.3, "readings": 2160 },
{ "meterId": "METER-003", "value": 102.0, "readings": 2160 }
],
"topPerformer": { "meterId": "METER-001", "value": 150.2, "readings": 2160 },
"bottomPerformer": { "meterId": "METER-002", "value": 85.3, "readings": 2160 }
}| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/admin/users |
List all users | Admin |
GET |
/admin/users/:id |
Get user details | Admin |
PUT |
/admin/users/:id |
Update user role/status | Admin |
DELETE |
/admin/users/:id |
Remove a user | Admin |
GET |
/admin/system |
System diagnostics | Admin |
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/meters |
List all registered meters | Yes |
GET |
/meters/:id |
Get meter details by ID | Yes |
POST |
/meters |
Register a new meter | Admin |
PUT |
/meters/:id |
Update meter configuration | Admin |
DELETE |
/meters/:id |
Remove a meter | Admin |
GET |
/meters/:id/readings |
Get readings for a specific meter | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/api/exports/readings |
Export meter readings (CSV/JSON/NDJSON) | Yes |
GET |
/api/exports/analytics/:summaryType |
Export analytics summaries (daily/weekly/monthly) | Yes |
GET |
/api/exports/system-report |
Export system-wide report (meters, readings, alerts) | Admin |
GET |
/api/exports/meters |
Export meter registry | Yes |
All export endpoints support the following query parameters:
format- Output format:csv,json, orndjson(default:csv)fields- Comma-separated list of fields to include (e.g.,id,timestamp,value)startDate- Filter by start date (ISO format:2026-01-01or2026-01-01T00:00:00Z)endDate- Filter by end date (ISO format)pretty- Set totruefor pretty-printed JSON (default:false)
Additional parameters specific to endpoints:
/api/exports/readings:meterIds(comma-separated),status/api/exports/analytics/:summaryType: Date range filtering for daily summaries/api/exports/system-report:sections(comma-separated:meters,readings,alerts,summary)/api/exports/meters:status,location
# Export all readings as CSV
curl -H "Authorization: Bearer YOUR_TOKEN" \
"http://localhost:3000/api/exports/readings?format=csv"
# Export specific fields as JSON
curl -H "Authorization: Bearer YOUR_TOKEN" \
"http://localhost:3000/api/exports/readings?format=json&fields=id,timestamp,value"
# Export readings for specific meters and date range
curl -H "Authorization: Bearer YOUR_TOKEN" \
"http://localhost:3000/api/exports/readings?format=csv&meterIds=meter-001,meter-002&startDate=2026-01-01&endDate=2026-06-01"
# Export daily analytics as NDJSON (streaming)
curl -H "Authorization: Bearer YOUR_TOKEN" \
"http://localhost:3000/api/exports/analytics/daily?format=ndjson"
# Export system report (admin only)
curl -H "Authorization: Bearer YOUR_TOKEN" \
-H "x-role: admin" \
"http://localhost:3000/api/exports/system-report?format=json§ions=meters,summary"All export endpoints use streaming to handle large datasets efficiently:
- Responses use
Transfer-Encoding: chunked - Data is streamed row-by-row for CSV and line-by-line for NDJSON
- Memory usage remains constant regardless of dataset size
- Suitable for exporting 10,000+ records
| Method | Path | Description | Auth |
|---|---|---|---|
POST |
/webhooks |
Register a webhook endpoint | Admin |
GET |
/webhooks |
List registered webhooks | Yes |
DELETE |
/webhooks/:id |
Remove a webhook | Admin |
| Event | Direction | Description |
|---|---|---|
meter:reading |
Server → Client | Real-time meter reading update |
meter:alert |
Server → Client | Meter anomaly or alert notification |
meter:status |
Server → Client | Meter online/offline status change |
subscribe:meters |
Client → Server | Subscribe to specific meter IDs |
unsubscribe:meters |
Client → Server | Unsubscribe from specific meter IDs |
Connect to ws://localhost:3000/ws.
GET /
{
"project": "Equipchain",
"status": "Monitoring Meters",
"contract": "CB7PSJZALNWNX7NLOAM6LOEL4OJZMFPQZJMIYO522ZSACYWXTZIDEDSS"
}List endpoints select a strategy with ?paginate=offset (default) or ?paginate=cursor.
Offset — ?page=2&limit=20. Familiar, supports jumping to an arbitrary page, and reports
total/totalPages. Best for small to medium result sets.
Cursor (keyset) — ?cursor=<opaque>&limit=20 forward, ?before=<opaque>&limit=20 backward.
Position is anchored to a value rather than a row count, which gives it two properties offset
paging cannot have: performance independent of how deep you are, and no page drift — rows
inserted or deleted between requests never cause a client to skip or repeat items. Use it for
large, append-heavy data such as meter readings, audit logs and webhook delivery logs.
| Parameter | Mode | Description |
|---|---|---|
paginate |
both | offset (default) or cursor |
limit |
both | Items per page, 1–100 (default 20) |
page |
offset | 1-based page number (default 1) |
cursor |
cursor | Page forward from this position |
before |
cursor | Page backward from this position |
sortBy / sortOrder |
both | Sort field and asc/desc |
GET /meters?paginate=cursor&limit=2
{
"data": [ { "id": 1 }, { "id": 2 } ],
"pagination": {
"limit": 2,
"cursor": null,
"nextCursor": "eyJ2IjoxLCJmIjoiaWQiLCJvIjoiYXNjIiwiayI6MiwiaWQiOjJ9",
"prevCursor": null,
"hasNext": true,
"hasPrev": false
}
}total and totalPages are absent in cursor mode: counting the full result set is the exact
cost cursor pagination exists to avoid. Endpoints that need a count can opt in via the
includeTotal option.
Cursors are opaque. Pass them back exactly as received — do not construct, parse or edit
them. A cursor records the sort it was issued for, so replaying one against a different
sortBy/sortOrder is rejected with a 400 rather than silently returning the wrong rows.
EquipChain-backend/
├── .github/
│ └── workflows/
│ └── ci.yml # CI pipeline (test on push/PR to main)
├── scripts/
│ └── seed-readings.js # Sample meter readings generator (auto-runs in dev)
├── src/
│ ├── config/
│ │ ├── logger.js # Pino structured logger
│ │ └── tracing.js # OpenTelemetry setup
│ ├── routes/
│ │ └── analytics.js # Analytics aggregation endpoints
│ ├── schemas/
│ │ ├── analytics.schema.js # Analytics query validation schemas
│ │ └── common.schema.js # Shared Zod query schemas
│ ├── services/
│ │ └── aggregator.js # In-memory aggregation engine
│ └── utils/
│ ├── errors.js # ValidationError (HTTP 400)
│ └── pagination.js # Offset + cursor pagination, filtering, sorting, search
├── test/
│ ├── aggregator.test.js # Aggregation service unit tests
│ ├── analytics.test.js # Analytics endpoint integration tests
│ ├── common.schema.test.js # Query schema tests
│ ├── cursorPagination.test.js # Cursor (keyset) pagination tests
│ ├── logger.test.js # Logger unit tests
│ ├── pagination.test.js # Pagination utility tests
│ └── server.test.js # Server integration tests
├── index.js # Express app entry point
├── package.json # Project metadata and dependencies
└── README.md # You are here
Request
│
▼
[Logger] → HTTP request logging (Pino + Correlation ID)
[Rate Limiter] → Rate limiting per IP/user
[CORS] → Cross-origin resource sharing
[Auth] → JWT verification for protected routes
[Validator] → Request body/param validation
│
▼
[Router] → Dispatches to the appropriate controller
│
▼
[Controller] → Handles business logic orchestration
[Service Layer] → Encapsulates domain logic
[Repository] → Data access (Redis / Soroban / DB)
│
▼
[Response] → JSON serialization and response
Services encapsulate business logic and are injected into controllers:
- MeterService — CRUD operations for meters, reading aggregation
- AuthService — User authentication, JWT management, session handling
- AnalyticsService — Trend computation, anomaly detection, alert generation
- ExportService — Data formatting and file generation (CSV/JSON)
| Repository | Backend | Purpose |
|---|---|---|
MeterRepository |
Soroban / Redis | On-chain meter state and cached readings |
UserRepository |
PostgreSQL / in-memory | User accounts and roles |
AnalyticsRepository |
Redis | Cached aggregations and computed metrics |
WebhookRepository |
Redis | Webhook endpoint storage and event dispatch |
All configuration is via environment variables. Create a .env file in the project root.
| Variable | Default | Required | Description |
|---|---|---|---|
PORT |
3000 |
No | Port the HTTP server binds to |
HOST |
0.0.0.0 |
No | Host address to bind |
CONTRACT_ID |
(see below) | No | Stellar Soroban contract ID for meter data |
REDIS_URL |
— | No | Redis connection string (redis://...) |
WS_ENABLED |
false |
No | Set to true to enable WebSocket server |
WS_PATH |
/ws |
No | WebSocket endpoint path |
NODE_ENV |
development |
No | development, test, or production |
LOG_LEVEL |
info |
No | debug, info, warn, error |
SKIP_SEED |
— | No | Set to 1 to skip auto-seeding sample data |
OTEL_EXPORTER_OTLP_ENDPOINT |
— | No | OTLP collector endpoint for trace export |
OTEL_SERVICE_NAME |
equipchain-api |
No | Service name reported in traces |
Default CONTRACT_ID: CB7PSJZALNWNX7NLOAM6LOEL4OJZMFPQZJMIYO522ZSACYWXTZIDEDSS
npm testTests use Node's built-in node:test and node:assert modules. No additional test framework is required.
Sample meter reading data spanning 90 days across 3 meters is auto-generated on server start in development mode. The seed data powers the analytics endpoints with realistic consumption patterns (morning ramp, peak hours, evening decline, night lows). To manually seed or re-seed:
node scripts/seed-readings.jsnpx eslint .npm ci --productiondocker build -t equipchain-backend .
docker run -p 3000:3000 --env-file .env equipchain-backendPerformance testing is implemented using Grafana k6, an open-source load testing tool to identify performance bottlenecks under stress.
Install k6 by following the official installation guide:
# macOS
brew install k6
# Ubuntu/Debian
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6
# Windows (winget)
winget install k6Verify installation:
k6 versionAll test scripts are located in the k6/ directory and are parameterizable via environment variables.
| Test | File | Description | Command |
|---|---|---|---|
| Smoke | k6/smoke.js |
1 VU performing all API operations for 30s. Verifies basic functionality under no load. | npm run k6:smoke |
| Load | k6/load.js |
Ramp up to 50 VUs over 1 min, sustain for 3 min, ramp down over 1 min. 80% reads, 20% writes. | npm run k6:load |
| Stress | k6/stress.js |
Gradual increase from 10 → 50 → 100 → 200 → 500 VUs to identify the breaking point. | npm run k6:stress |
| Spike | k6/spike.js |
Sudden jump from 0 to 200 VUs in 10s, sustain for 1 min, then cool down. | npm run k6:spike |
| Soak | k6/soak.js |
50 VUs sustained for 30+ minutes to detect memory leaks and performance degradation. | npm run k6:soak |
| Quick | (all except soak) | Runs smoke, load, stress, and spike tests sequentially. | npm run k6:quick |
Override the base URL and other parameters via environment variables:
# Point to a different environment
k6 run k6/smoke.js -e BASE_URL=https://staging.example.com
# Override soak test duration and concurrency
k6 run k6/soak.js -e DURATION=60m -e VUS=100Each test measures and reports:
| Metric | Description |
|---|---|
| Request Rate (RPS) | Number of requests per second |
| Response Time Percentiles | p50, p75, p90, p95, p99 — median and tail latency |
| Error Rate | Percentage of failed/non-2xx requests |
| Checks | Application-level assertions (e.g., status is 200, body has required fields) |
Generate visual HTML reports for detailed analysis:
k6 run --out html=k6/results/load-report.html k6/load.jsThe standard CI workflow (.github/workflows/ci.yml) runs unit tests only (npm test). For load testing in CI, add a separate workflow step that installs k6 and runs the smoke test as a quick health check:
- name: Install k6
run: |
curl -fsSL https://github.com/grafana/k6/releases/download/v0.54.0/k6-v0.54.0-linux-amd64.tar.gz | tar -xz
sudo cp k6-v0.54.0-linux-amd64/k6 /usr/local/bin/
- name: Run k6 smoke test
run: k6 run k6/smoke.js
env:
BASE_URL: ${{ secrets.BASE_URL }}Test results (HTML reports, JSON summaries) are stored in k6/results/. This directory is gitignored and will not be committed to the repository.
docker build -t equipchain-backend .
docker run -d \
--name equipchain-api \
-p 3000:3000 \
--env-file .env \
equipchain-backend| Platform | Instructions |
|---|---|
| Railway | Connect repo, set build command npm install, start command npm start |
| Render | Use Web Service, set runtime to Node, start command npm start |
| Fly.io | Run fly launch, configure internal_port = 3000 |
| AWS ECS | Push Docker image to ECR, configure task definition with env vars |
The included GitHub Actions workflow (.github/workflows/ci.yml) runs on every push and pull request to main:
- Checkout — Clone the repository
- Setup Node — Install Node.js 22 with npm cache
- Install —
npm ci - Test —
npm test
- Set
NODE_ENV=productionto disable debug logging and auto-seeding - Use a reverse proxy (nginx, Caddy) for SSL termination
- Configure health check monitoring on
/api/health
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please ensure tests pass before submitting.
- EquipChain Contracts — Soroban smart contracts on Stellar
- EquipChain Frontend — Web dashboard and client app
MIT © EquipChain