REST API Patterns
Common patterns for authentication, pagination, error handling, and file uploads across Neostra APIs.
REST API Patterns
All Neostra services follow consistent REST API patterns for authentication, request/response formatting, pagination, error handling, and file uploads. This guide covers the conventions shared across all services.
Base URL
All API endpoints use the /api/v1 prefix:
https://api.neostra.io/v1
Authentication
All authenticated endpoints require a Bearer JWT token in the Authorization header.
curl -X GET https://api.neostra.io/v1/assessments \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
See the Authentication guide for details on obtaining and managing JWT tokens.
Response Format
Every API response follows the ServiceResponse envelope:
{
"success": true,
"message": "Operation completed successfully",
"data": {
// Response payload
}
}
| Field | Type | Description |
|---|---|---|
success | Boolean | Whether the request was successful |
message | String | Human-readable status message |
data | Object | The response payload. Structure varies by endpoint. |
Success Response Example
{
"success": true,
"message": "Assessment created successfully",
"data": {
"id": "assessment-abc-123",
"name": "Website Privacy Assessment",
"status": "draft",
"createdAt": "2026-03-15T10:30:00Z"
}
}
Error Response Example
{
"success": false,
"message": "Validation failed",
"data": {
"errors": [
{
"field": "name",
"message": "Name is required"
},
{
"field": "brandId",
"message": "Invalid brand ID"
}
]
}
}
Pagination
List endpoints support pagination through query parameters. Paginated responses wrap results in a PaginatedResult structure.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
skip | Integer | 0 | Number of records to skip |
limit | Integer | 20 | Maximum number of records to return |
sortField | String | createdAt | Field to sort by |
sortOrder | String | desc | Sort direction: asc or desc |
searchTerm | String | -- | Full-text search filter |
Paginated Response
{
"success": true,
"message": "Records retrieved",
"data": {
"totalCount": 156,
"skip": 0,
"limit": 20,
"results": [
{ "id": "item-001", "name": "First Item" },
{ "id": "item-002", "name": "Second Item" }
]
}
}
Pagination Examples
# First page, 20 items, sorted by name ascending
curl -X GET "https://api.neostra.io/v1/assessments?skip=0&limit=20&sortField=name&sortOrder=asc" \
-H "Authorization: Bearer <token>"
# Second page
curl -X GET "https://api.neostra.io/v1/assessments?skip=20&limit=20&sortField=name&sortOrder=asc" \
-H "Authorization: Bearer <token>"
# Search with pagination
curl -X GET "https://api.neostra.io/v1/assessments?searchTerm=privacy&skip=0&limit=10" \
-H "Authorization: Bearer <token>"
async function fetchPaginated(endpoint, { skip = 0, limit = 20, sortField, sortOrder, searchTerm } = {}) {
const params = new URLSearchParams({ skip, limit });
if (sortField) params.set("sortField", sortField);
if (sortOrder) params.set("sortOrder", sortOrder);
if (searchTerm) params.set("searchTerm", searchTerm);
const response = await fetch(
`https://api.neostra.io/v1/${endpoint}?${params}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const { data } = await response.json();
return data; // { totalCount, skip, limit, results }
}
// Usage
const page1 = await fetchPaginated("assessments", { limit: 10 });
const page2 = await fetchPaginated("assessments", { skip: 10, limit: 10 });
import requests
def fetch_paginated(endpoint, skip=0, limit=20, sort_field=None, sort_order=None, search=None):
params = {"skip": skip, "limit": limit}
if sort_field:
params["sortField"] = sort_field
if sort_order:
params["sortOrder"] = sort_order
if search:
params["searchTerm"] = search
response = requests.get(
f"https://api.neostra.io/v1/{endpoint}",
headers={"Authorization": f"Bearer {token}"},
params=params,
)
return response.json()["data"]
# Usage
page = fetch_paginated("assessments", skip=0, limit=10, search="privacy")
print(f"Total: {page['totalCount']}, Returned: {len(page['results'])}")
Error Handling
HTTP Status Codes
| Status | Meaning | When It Occurs |
|---|---|---|
200 | OK | Successful read or update |
201 | Created | Successful resource creation |
400 | Bad Request | Validation errors, malformed input |
401 | Unauthorized | Missing or expired JWT token |
403 | Forbidden | Valid token but insufficient permissions |
404 | Not Found | Resource does not exist |
500 | Internal Server Error | Unexpected server-side failure |
Field-Level Validation Errors
When a 400 response is returned for validation failures, the data.errors array contains field-level details:
{
"success": false,
"message": "Validation failed",
"data": {
"errors": [
{
"field": "email",
"message": "Must be a valid email address"
},
{
"field": "password",
"message": "Must be at least 8 characters"
}
]
}
}
Error Handling Example
async function apiRequest(method, endpoint, body = null) {
const options = {
method,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
};
if (body) options.body = JSON.stringify(body);
const response = await fetch(`https://api.neostra.io/v1/${endpoint}`, options);
const result = await response.json();
if (!response.ok) {
switch (response.status) {
case 400:
// Handle validation errors
const fieldErrors = result.data?.errors || [];
console.error("Validation errors:", fieldErrors);
break;
case 401:
// Token expired, redirect to login
redirectToLogin();
break;
case 403:
// Insufficient permissions
console.error("Access denied:", result.message);
break;
default:
console.error("API error:", result.message);
}
throw new Error(result.message);
}
return result.data;
}
import requests
def api_request(method, endpoint, body=None):
response = requests.request(
method,
f"https://api.neostra.io/v1/{endpoint}",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json=body,
)
result = response.json()
if not response.ok:
if response.status_code == 400:
errors = result.get("data", {}).get("errors", [])
for error in errors:
print(f" {error['field']}: {error['message']}")
elif response.status_code == 401:
print("Token expired. Please re-authenticate.")
elif response.status_code == 403:
print(f"Access denied: {result['message']}")
raise Exception(result["message"])
return result["data"]
Permissions
API endpoints enforce fine-grained permissions using @PreAuthorize annotations. Permissions follow the resource:action format.
A 403 Forbidden response means the authenticated user does not have the required permission. Check the user's role and permission assignments in the platform settings.
Common Permissions
| Endpoint Pattern | Required Permission |
|---|---|
GET /api/v1/assessments | assessments:view |
POST /api/v1/assessments | assessments:create |
PUT /api/v1/assessments/:id | assessments:edit |
DELETE /api/v1/assessments/:id | assessments:delete |
GET /api/v1/subject-requests | subject-requests:view |
POST /api/v1/subject-requests | subject-requests:create |
GET /api/v1/users | users:view |
POST /api/v1/users/invite | users:create |
GET /api/v1/consent | consent:view |
PUT /api/v1/consent/config | consent:manage |
File Upload
File uploads use multipart/form-data with the following constraints:
| Constraint | Value |
|---|---|
| Max file size | 10 MB |
| Max request size | 50 MB |
| Endpoint | POST /api/v1/storage/file-upload |
curl -X POST https://api.neostra.io/v1/storage/file-upload \
-H "Authorization: Bearer <token>" \
-F "file=@/path/to/document.pdf" \
-F "context=assessment" \
-F "referenceId=assessment-abc-123"
const formData = new FormData();
formData.append("file", fileInput.files[0]);
formData.append("context", "assessment");
formData.append("referenceId", "assessment-abc-123");
const response = await fetch("https://api.neostra.io/v1/storage/file-upload", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
// Do not set Content-Type — the browser sets it with the boundary
},
body: formData,
});
const { data } = await response.json();
// data contains: { fileId, fileName, fileUrl, mimeType, size }
import requests
with open("/path/to/document.pdf", "rb") as f:
response = requests.post(
"https://api.neostra.io/v1/storage/file-upload",
headers={"Authorization": f"Bearer {token}"},
files={"file": ("document.pdf", f, "application/pdf")},
data={"context": "assessment", "referenceId": "assessment-abc-123"},
)
result = response.json()["data"]
print(f"Uploaded: {result['fileName']} ({result['size']} bytes)")
Upload Response
{
"success": true,
"message": "File uploaded successfully",
"data": {
"fileId": "file-xyz-789",
"fileName": "document.pdf",
"fileUrl": "https://storage.neostra.io/uploads/file-xyz-789/document.pdf",
"mimeType": "application/pdf",
"size": 245760
}
}
Correlation IDs
Every API request is assigned a correlation ID by the CustomRequestInterceptor. This ID is returned in the response headers and is used for distributed tracing across services.
X-Correlation-Id: 7f3a8b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c
# The correlation ID is in the response headers
curl -v -X GET https://api.neostra.io/v1/assessments \
-H "Authorization: Bearer <token>" 2>&1 | grep X-Correlation-Id
# < X-Correlation-Id: 7f3a8b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c
const response = await fetch("https://api.neostra.io/v1/assessments", {
headers: { Authorization: `Bearer ${token}` },
});
const correlationId = response.headers.get("X-Correlation-Id");
console.log(`Request traced as: ${correlationId}`);
When reporting issues or debugging failures, include the X-Correlation-Id from the response headers. It allows tracing the request across all backend services.
Rate Limiting
API rate limits are determined by the tenant's subscription plan. When limits are exceeded, the API returns a 429 Too Many Requests response with a Retry-After header.
{
"success": false,
"message": "Rate limit exceeded. Please retry after 60 seconds.",
"data": null
}
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per minute |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the rate limit resets |
Retry-After | Seconds to wait before retrying (on 429 responses) |
Last updated 1 week ago
Built with Documentation.AI