Skip to content

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.

  1. The web server itself
  2. 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.

typescript
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:

typescript
.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.

typescript
.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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
.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.

typescript
.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.

typescript
.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.

typescript
.markAsDeprecated() 
.setCommandFunction(async function (context, payload, parameter) {
  // business implementation
})