# Expose as HTTP Endpoint

How to expose a PURISTA service command as HTTP endpoint

---
Canonical: /handbook/2_building_business-logic/command/exposing-a-command-as-http-endpoint/
Source: web/src/content/handbook/2_building_business-logic/command/exposing-a-command-as-http-endpoint.md
Format: Markdown for agents
---

# Expose as HTTP endpoint

A command is not exposing itself via HTTP.  
To expose commands, you can use [Hono based HTTP server](../../3_eco_system/http_server.md).  
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 accessible 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')  // [!code ++]
  .setCommandFunction(async function (context, payload, parameter) {
    // implement your logic here
  })
```

In the example above, we define that the command is accessible 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]`.

::: info ⚠️ 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
)
```

This is very powerful 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' },
)
```

::: info 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 the `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( // [!code ++]
      {  // [!code ++]
        name: 'param', // [!code ++]
        required: false // [!code ++]
      },  // [!code ++]
      {   // [!code ++]
        required: true, // [!code ++]
        name: 'required' // [!code ++]
      }  // [!code ++]
    )  // [!code ++]
  .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( // [!code ++]
        z.string().optional(),  // [!code ++]
        { title: 'The optional query parameter param', example: 'some_id' } // [!code ++]
      ) // [!code ++]
    neededParam: extendApi( // [!code ++]
        z.string(),  // [!code ++]
        { title: 'The required query parameter neededParam', example: 'some_id' } // [!code ++]
      ) // [!code ++]
  }),
  { title: 'The parameter object' },
)
```

::: info Query parameters are strings
Query parameters are always provided as string type.  
Conversion must be implemented via transformers (preferred) 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')
  .makeEndpointPublic() // [!code ++]
  .addQueryParameters(
      { 
        name: 'param',
        required: false
      }, 
      {  
        required: true,
        name: 'required'
      } 
    ) 
  .setCommandFunction(async function (context, payload, parameter) {
    // implement your logic here
  })
```

## OpenApi information

PURISTA provides OpenAPI schema generation out of the box, based on the provided [Zod schemas](https://zod.dev) 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](../error-handling.md) 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) // [!code ++]
.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') // [!code ++]
.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') // [!code ++]
.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, all commands of this service version are marked as deprecated.

Every command can be marked as deprecated individually by using the `markAsDeprecated` method.

```typescript
.markAsDeprecated() // [!code ++]
.setCommandFunction(async function (context, payload, parameter) {
  // business implementation
})
```
