Expose as HTTP endpoint
A command is not exposing itself via HTTP.
To expose commands, you can use Hono based HTTP server.
This means we have basically two parts.
- The web server itself
- The service instance with the command(s)
Both are communicating over the event bridge.
In the command builder, you can define the information which is needed to expose a command. The web server service will build the router and OpenApi definition based on this information.
Define the URL
If you want to make a command accessable via an HTTP endpoint, you need to define the HTTP method and the url path.
import {
myServiceV1MyCommandInputPayloadSchema,
myServiceV1MyCommandInputParameterSchema,
myServiceV1MyCommandOutputSchema,
} from './schema.js'
const myCommandBuilder = myServiceBuilder
.getCommandBuilder('functionName', 'some function description', 'functionEventEmitted')
.addPayloadSchema(myServiceV1MyCommandInputPayloadSchema)
.addParameterSchema(myServiceV1MyCommandInputParameterSchema)
.addOutputSchema(myServiceV1MyCommandOutputSchema)
.exposeAsHttpEndpoint('GET', 'ping')
.setCommandFunction(async function (context, payload, parameter) {
// implement your logic here
})
In the example above, we define, that the command is accessable via GET
method.
The given path ping
will result in final url path /api/v1/ping
.
The first part /api
is a configurable value in PURISTA HTTP servers, which defaults to api
.
The second part v1
will be autogenerated, based on the service version - v[SERVICE_VERSION]
.
⚠️ Be aware
GET endpoints will not have a payload and the payload schema should be set to z.undefined()
.
If your command is not returning a value, you should set the output schema to z.void()
.
Empty responses are returned with status code 204 No Content.
Non-JSON payload
The exposeAsHttpEndpoint
has optional parameters for content type and content encoding, which can be used to handle payloads, which are not JSON content.
As an example:
.exposeAsHttpEndpoint(
'GET', // HTTP method
'ping', // url path
'text/csv', // request content type
'utf-8', // request content encoding
'application/pdf', // response content type
'utf-8' // response content encoding
)
It is a very powerfull in combination with input/output transformer functions.
Path parameter
To define path parameters, 2 things are needed.
First, you need to define the parameter in the url path.
You simply need to add :[parameterName]
to your path.
.exposeAsHttpEndpoint('GET', 'domain/:id')
As path parameters are automatically injected into the parameter object, you need to add the path parameter to the parameter schema.
export const theServiceV1PingInputParameterSchema = extendApi(
z.object({
id: extendApi(z.string(), { title: 'The id path parameter', example: 'some_id' } )
}),
{ title: 'The parameter object' },
)
Optional path parameters
You can define a path parameter as optional by adding ?
like 'domain/:id'
.
Do not forget to mark it in the schema as optional z.string().optional()
or set a default value z.string().default('some_default')
.
Adding query parameters
In case you need to use query parameters, you can use zhe addQueryParameters
method of the command definition builder.
import {
myServiceV1MyCommandInputPayloadSchema,
myServiceV1MyCommandInputParameterSchema,
myServiceV1MyCommandOutputSchema,
} from './schema.js'
const myCommandBuilder = myServiceBuilder
.getCommandBuilder('functionName', 'some function description', 'functionEventEmitted')
.addPayloadSchema(myServiceV1MyCommandInputPayloadSchema)
.addParameterSchema(myServiceV1MyCommandInputParameterSchema)
.addOutputSchema(myServiceV1MyCommandOutputSchema)
.exposeAsHttpEndpoint('GET', 'ping')
.addQueryParameters(
{
name: 'param',
required: false
},
{
neededParam: true,
name: 'required'
}
)
.setCommandFunction(async function (context, payload, parameter) {
// implement your logic here
})
Query parameters are provided in the parameter object.
Because of this, you need to add them into the parameter input schema object.
export const theServiceV1PingInputParameterSchema = extendApi(
z.object({
id: extendApi(z.string(), { title: 'The id path parameter', example: 'some_id' } )
param: extendApi(
z.string().optional(),
{ title: 'The optional query parameter param', example: 'some_id' }
)
neededParam: extendApi(
z.string().optional(),
{ title: 'The required query parameter neededParam', example: 'some_id' }
)
}),
{ title: 'The parameter object' },
)
Quer parameters are strings
Query parameters are always provided as string type.
Conversation must be implemented via transformers (prefferred) or inside the business logic.
Security
API endpoints might be protected by the web server. By default, all endpoints are designated as protected, and each public endpoint must be explicitly marked as not secured.
import {
myServiceV1MyCommandInputPayloadSchema,
myServiceV1MyCommandInputParameterSchema,
myServiceV1MyCommandOutputSchema,
} from './schema.js'
const myCommandBuilder = myServiceBuilder
.getCommandBuilder('functionName', 'some function description', 'functionEventEmitted')
.addPayloadSchema(myServiceV1MyCommandInputPayloadSchema)
.addParameterSchema(myServiceV1MyCommandInputParameterSchema)
.addOutputSchema(myServiceV1MyCommandOutputSchema)
.exposeAsHttpEndpoint('GET', 'ping')
.disableHttpSecurity()
.addQueryParameters(
{
name: 'param',
required: false
},
{
neededParam: true,
name: 'required'
}
)
.setCommandFunction(async function (context, payload, parameter) {
// implement your logic here
})
The disableHttpSecurity
also has an optional flag, which might be helpfull during development.
There is also the opposite method enableHttpSecurity
available.
OpenApi information
PURISTA provides OpenAPI schema generation out of the box, based on the provided Zod schemas and builder options.
There are a few helpers, to improve the schema.
Additional error codes
For example, your command might need to return a 401 Unauthorized or 403 Forbidden.
This can be done by throwing a HandledError with corresponding status code set.
The correct error response will already be sent to the client, but the status code is not listed in the OpenApi definition.
To solve this, you simply use the addOpenApiErrorStatusCodes
method, to add error response status codes.
.addOpenApiErrorStatusCodes(StatusCode.Unauthorized)
.setCommandFunction(async function (context, payload, parameter) {
throw new HandledError(StatusCode.Unauthorized)
})
Adding tags
OpenAPI provides the possibility, to assign tags to endpoints. To assign one or more tags to your endpoint, the command builder provides the addOpenApiTags
method.
.addOpenApiTags('a-tag', 'additional-tag')
.setCommandFunction(async function (context, payload, parameter) {
// business implementation
})
Summary
By default, the command name will be used as OpenApi summary.
You can overwrite this with your own text.
.setOpenApiSummary('custom summary')
.setCommandFunction(async function (context, payload, parameter) {
// business implementation
})
Mark as deprecated
There are two possible ways to mark a command as deprecated.
Commands are automatically marked as deprecated, as soon as the parent service version is marked as deprecated. In this case, alle commands of this service version are marked as deprecated.
Every command can be marked as deprecated individually by using the markAsDeprecated
method.
.markAsDeprecated()
.setCommandFunction(async function (context, payload, parameter) {
// business implementation
})