Response Envelope
All API endpoints in The Shift Platform return responses in a consistent envelope format. This makes it straightforward for agents and clients to determine whether a request succeeded and to extract either the data or the error details.
Response Format
Success
{
"success": true,
"data": {
"id": "abc12345",
"name": "auth-service",
"language": "TypeScript"
}
}
Error
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Service \"nonexistent\" not found"
}
}
Errors may optionally include a details field with additional context:
{
"success": false,
"error": {
"code": "BAD_REQUEST",
"message": "Validation failed",
"details": {
"field": "name",
"reason": "Name must be at least 2 characters"
}
}
}
TypeScript Types
The response types are defined in @shift/platform-core/api/response:
interface ApiSuccess<T = unknown> {
success: true;
data: T;
}
interface ApiError {
success: false;
error: {
code: string;
message: string;
details?: unknown;
};
}
type ApiResponse<T = unknown> = ApiSuccess<T> | ApiError;
Helper Functions
ok(data)
Creates a success response:
import { ok } from "@shift/platform-core/api/response";
app.get("/services/:id", async (c) => {
const service = await store.getById(c.req.param("id"));
return c.json(ok(service));
});
Produces:
{ "success": true, "data": { "id": "abc12345", "name": "auth-service" } }
err(code, message, details?)
Creates an error response:
import { err } from "@shift/platform-core/api/response";
app.get("/services/:id", async (c) => {
const service = await store.getById(c.req.param("id"));
if (!service) {
return c.json(err("NOT_FOUND", `Service "${id}" not found`), 404);
}
return c.json(ok(service));
});
Produces:
{ "success": false, "error": { "code": "NOT_FOUND", "message": "Service \"abc12345\" not found" } }
Error Handling Middleware
The errorHandler() from @shift/platform-core/api/error-handler automatically catches thrown errors and converts them into the envelope format. It is installed on the gateway:
import { errorHandler } from "@shift/platform-core/api/error-handler";
const app = new Hono();
app.onError(errorHandler());
When an error is thrown in any route handler, the middleware:
- Checks if it is an
HttpErrorwith a known status code. - Maps the status to an error code and returns the envelope.
- For unexpected errors (status 500), logs the error server-side and returns a generic message to avoid leaking internals.
HttpError Class
The HttpError class allows route handlers to throw errors with specific HTTP status codes:
import { HttpError } from "@shift/platform-core/api/error-handler";
throw new HttpError(400, "Name is required");
// -> { "success": false, "error": { "code": "BAD_REQUEST", "message": "Name is required" } }
Factory Functions
For common error cases, factory functions throw the appropriate HttpError:
import {
badRequest,
notFound,
conflict,
} from "@shift/platform-core/api/error-handler";
// Throws HttpError(400, message)
badRequest("Name is required");
// Throws HttpError(404, message)
notFound("Service not found");
// Throws HttpError(409, message)
conflict("A service with that name already exists");
These functions have a return type of never, so TypeScript knows that code after them is unreachable.
Error Code Reference
| HTTP Status | Error Code | Factory Function |
|---|---|---|
| 400 | BAD_REQUEST | badRequest(message) |
| 404 | NOT_FOUND | notFound(message) |
| 409 | CONFLICT | conflict(message) |
| 500 | INTERNAL_ERROR | (automatic for unhandled errors) |
Client-Side Usage
When consuming the API, check the success field to determine the response type:
const res = await fetch("/api/v1/catalog/services/abc12345");
const body: ApiResponse<Service> = await res.json();
if (body.success) {
console.log(body.data.name); // TypeScript narrows to ApiSuccess<Service>
} else {
console.error(body.error.code, body.error.message); // Narrows to ApiError
}
The discriminated union on success gives full type narrowing in TypeScript, so you get type-safe access to either data or error without casting.