Skip to content

REST API Guide

The API Gateway automatically generates RESTful HTTP endpoints from your service contracts. Services never handle HTTP - they define contracts, and REST endpoints are created automatically.

The API Gateway generates RESTful URLs following standard conventions:

POST /api/users → CreateUserCommand
GET /api/users/:id → GetUserQuery
PUT /api/users/:id → UpdateUserCommand
DELETE /api/users/:id → DeleteUserCommand
GET /api/users → ListUsersQuery

Key Pattern: Resource names are derived from contract message types:

  • CreateUserCommand/api/users
  • GetOrderQuery/api/orders/:id
  • ListProductsQuery/api/products
Contract TypeHTTP MethodExample
Create*CommandPOSTPOST /api/users
Update*CommandPUTPUT /api/users/:id
Delete*CommandDELETEDELETE /api/users/:id
Get*QueryGETGET /api/users/:id
List*QueryGETGET /api/users
*CommandPOSTPOST /api/actions
*QueryGETGET /api/data

Create a contract in your service:

contracts/CreateUserContract.ts
import { createContract } from '@banyanai/platform-contract-system';
export const CreateUserContract = createContract({
messageType: 'CreateUserCommand',
targetService: 'user-service',
inputSchema: {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
name: { type: 'string' },
role: { type: 'string', enum: ['user', 'admin'] }
},
required: ['email', 'name']
},
outputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string' },
name: { type: 'string' },
role: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' }
},
required: ['id', 'email', 'name', 'role', 'createdAt']
},
requiredPermissions: ['users:create']
});

Write only business logic:

commands/CreateUserHandler.ts
import { CommandHandler } from '@banyanai/platform-cqrs';
import { CreateUserContract } from '../contracts/CreateUserContract.js';
@CommandHandler(CreateUserContract)
export class CreateUserHandler {
async handle(input: { email: string; name: string; role?: string }) {
// Pure business logic - no HTTP code
const user = await this.userRepository.create({
email: input.email,
name: input.name,
role: input.role || 'user'
});
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
createdAt: user.createdAt
};
}
}

The API Gateway creates:

Terminal window
POST http://localhost:3003/api/users
Terminal window
curl -X POST http://localhost:3003/api/users \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"name": "Alice Smith",
"role": "user"
}'

Response:

{
"id": "usr_1234567890",
"email": "alice@example.com",
"name": "Alice Smith",
"role": "user",
"createdAt": "2025-11-15T10:30:00Z"
}
Terminal window
# Get single resource
curl -X GET http://localhost:3003/api/users/usr_1234567890 \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# List resources
curl -X GET http://localhost:3003/api/users?limit=10&offset=0 \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Terminal window
curl -X PUT http://localhost:3003/api/users/usr_1234567890 \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Alice Johnson",
"role": "admin"
}'
Terminal window
curl -X DELETE http://localhost:3003/api/users/usr_1234567890 \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

For queries with IDs, the API Gateway automatically extracts path parameters:

// GetUserQuery contract
export const GetUserContract = createContract({
messageType: 'GetUserQuery',
targetService: 'user-service',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' }
},
required: ['id']
},
outputSchema: { /* user object */ }
});

Generated endpoint:

GET /api/users/:id

The id parameter is extracted and passed to the handler:

Terminal window
curl http://localhost:3003/api/users/usr_123
# Handler receives: { id: "usr_123" }

For list endpoints, query parameters are passed to handlers:

Terminal window
GET /api/users?limit=10&offset=0&role=admin
# Handler receives: { limit: 10, offset: 0, role: "admin" }

The API Gateway automatically maps responses to HTTP status codes:

ScenarioStatus CodeDescription
Success (Command)201 CreatedResource created
Success (Query)200 OKData retrieved
Success (Update)200 OKResource updated
Success (Delete)204 No ContentResource deleted
Validation Error400 Bad RequestInvalid input
Unauthorized401 UnauthorizedMissing/invalid auth
Forbidden403 ForbiddenInsufficient permissions
Not Found404 Not FoundResource doesn’t exist
Server Error500 Internal Server ErrorHandler exception

Errors are returned in a standard format:

{
"error": "ValidationError",
"message": "Invalid email format",
"correlationId": "cor_abc123xyz",
"timestamp": "2025-11-15T10:30:00Z",
"details": {
"field": "email",
"constraint": "format"
}
}

The API Gateway enables CORS by default:

// Configured in API Gateway
{
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}

The API Gateway generates OpenAPI/Swagger documentation automatically:

Terminal window
# Access Swagger UI
http://localhost:3003/api-docs
# Download OpenAPI spec
http://localhost:3003/api-docs/openapi.json
// Good: Resource-oriented
CreateUserCommand → POST /api/users
GetUserQuery → GET /api/users/:id
// Avoid: Action-oriented (still works but less RESTful)
RegisterUserCommand → POST /api/register-user
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
format: 'email',
maxLength: 255
},
age: {
type: 'number',
minimum: 0,
maximum: 150
}
},
required: ['email']
}
export const CreateUserContract = createContract({
messageType: 'CreateUserCommand',
targetService: 'user-service',
requiredPermissions: ['users:create'], // ← Enforce permissions
// ...
});
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
format: 'email',
description: 'User email address for authentication'
},
name: {
type: 'string',
description: 'Full name of the user'
}
}
}

Cause: Service not registered or contract not discovered

Solution: Check contract cache:

Terminal window
curl http://localhost:3003/debug/contracts

Verify your service is running and contracts are broadcast.

Cause: Missing or invalid JWT token

Solution: Include valid Bearer token:

Terminal window
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:3003/api/users

Cause: User lacks required permissions

Solution: Verify user has permissions in contract’s requiredPermissions:

requiredPermissions: ['users:create']