API Design Lessons from Building an OMS
API Design Lessons from Building an OMS
Vectis has 150+ API endpoints. Here’s what we learned designing them.
Lesson 1: Idempotency Is Non-Negotiable
Networks fail. Clients retry. If your POST endpoint creates a duplicate resource on retry, you’ve got a problem.
Every mutating endpoint in Vectis accepts an Idempotency-Key header:
POST /api/v1/orders
Idempotency-Key: order-create-abc123
If we’ve seen that key before, we return the original response. No duplicate order created.
Implementation: Store idempotency keys with their responses in Redis, expire after 24 hours. Check before processing, store after success.
Lesson 2: Pagination Done Right
Offset pagination (?page=2&per_page=50) is simple but breaks when data changes. If a new order is inserted while you’re paginating, you’ll either skip an item or see one twice.
Cursor pagination is more robust:
GET /api/v1/orders?cursor=eyJpZCI6MTIzNH0&limit=50
The cursor encodes the last item’s position. New inserts don’t affect your traversal.
We use offset pagination for simple cases (admin dashboards where consistency isn’t critical) and cursor pagination for integrations that need to reliably process all records.
Lesson 3: Consistent Error Format
Every error returns the same structure:
{
"error": {
"code": "validation_error",
"message": "SKU not found: WIDGET-123",
"field": "items[0].sku",
"details": {
"provided": "WIDGET-123",
"suggestion": "WIDGET-124"
}
}
}
Clients can always expect:
code: Machine-readable error typemessage: Human-readable explanationfield: Which input field caused the error (if applicable)details: Additional context (optional)
No guessing whether the error is in error, errors, message, or detail.
Lesson 4: Verb the Nouns
REST purists say use HTTP methods for actions. In practice, some actions don’t map cleanly:
POST /api/v1/orders/123/ship
POST /api/v1/orders/123/cancel
POST /api/v1/inventory/adjust
These are clearer than trying to shoehorn everything into PATCH:
PATCH /api/v1/orders/123
{ "status": "shipped" } // What about tracking number? Carrier? Ship date?
Use sub-resource endpoints for actions that have their own parameters or side effects.
Lesson 5: Expand Related Resources
Fetching an order, then fetching its items, then fetching each product is slow. Allow expansion:
GET /api/v1/orders/123?expand=items,items.product,packages
Returns the order with items, products, and packages embedded. One request instead of N+1.
But be careful: deep expansion can return massive payloads. We limit expansion depth and document which expansions are available per endpoint.
Lesson 6: Webhooks Need Signatures
Anyone can POST to your webhook URL. Without verification, you’re trusting random internet traffic.
Every Vectis webhook includes an HMAC-SHA256 signature:
X-Vectis-Signature: sha256=abc123...
Computed as:
const signature = crypto
.createHmac('sha256', webhookSecret)
.update(requestBody)
.digest('hex');
Clients verify the signature before processing. If it doesn’t match, reject the request.
Lesson 7: Rate Limits with Feedback
Rate limiting protects your system. But silent failures frustrate developers.
Every response includes rate limit headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 23
X-RateLimit-Reset: 1705312800
When limited, return 429 with a clear message:
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 2025-01-15T10:00:00Z",
"retry_after": 60
}
}
Developers can build backoff logic. No guessing why requests are failing.
Lesson 8: Scoped API Keys
One API key with full access is a security incident waiting to happen.
Vectis API keys have scopes:
orders:read
orders:write
inventory:read
inventory:write
products:read
webhooks:manage
A key for your reporting dashboard only needs orders:read. A key for your WMS integration needs orders:write and inventory:write. Principle of least privilege.
Lesson 9: Versioning Strategy
We version via URL path: /api/v1/orders. When we make breaking changes, we’ll release /api/v2/orders.
Non-breaking changes (new optional fields, new endpoints) go into the current version. Breaking changes (removed fields, changed behavior) require a new version.
We commit to supporting old versions for 12 months after a new version releases. Gives integrators time to migrate.
Lesson 10: Document Everything
Every endpoint has:
- Description of what it does
- Request parameters with types and validation rules
- Response schema with examples
- Error codes it can return
- Rate limit tier
We use OpenAPI 3.0 and generate docs automatically. Interactive Swagger UI lets developers try endpoints without writing code.
The best API is one developers can figure out from the docs without contacting support.
Vectis has 150+ OpenAPI-documented endpoints. Explore the API.