Skip to main content

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:

  1. Checks if it is an HttpError with a known status code.
  2. Maps the status to an error code and returns the envelope.
  3. 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 StatusError CodeFactory Function
400BAD_REQUESTbadRequest(message)
404NOT_FOUNDnotFound(message)
409CONFLICTconflict(message)
500INTERNAL_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.