> ## Documentation Index
> Fetch the complete documentation index at: https://docs.jojapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Target Transformations

> Request and Response Transformations at the API Gateway

When you configure an API Target, the gateway acts as a proxy between your API consumers and your backend server. By default, requests are forwarded as-is to your target server. However, you often need to modify requests before they reach your backend or transform responses before they're returned to consumers.

Target Transformations allow you to:

* **Rewrite paths** - Map consumer-facing URLs to your internal API structure
* **Inject headers** - Add authentication tokens, API keys, or tracking headers
* **Modify query parameters** - Add, remove, or transform URL parameters
* **Transform request bodies** - Restructure payloads, add metadata, or wrap content
* **Transform responses** - Filter fields, restructure data, or normalize formats
* **Calculate dynamic credits** - Charge based on actual usage from response data

All transformations use an [Expression Language](https://symfony.com/doc/current/components/expression_language.html) that lets you access request data, user context, and response data dynamically.

## Expression Syntax

Expressions are enclosed in double curly braces:

```
{{ expression }}
```

Everything outside `{{ }}` is treated as static text. You can mix static content with multiple expressions:

```
/api/v2{{ request.path }}?user={{ context.userId }}
```

## Where Expressions Can Be Used

Expressions are supported in the following Target configuration fields:

| Field                   | Tab      | Description                                |
| ----------------------- | -------- | ------------------------------------------ |
| Path Transformation     | Request  | Rewrite the URL path sent to your backend  |
| Custom Headers          | Request  | Add or modify headers sent to your backend |
| Query Parameters        | Request  | Add or modify URL query parameters         |
| Request Body            | Request  | Transform the request payload              |
| Custom Response Headers | Response | Add or modify headers returned to consumer |
| Response Body           | Response | Transform the response payload             |
| Dynamic Credits         | Response | Calculate credits based on response data   |

## Context Variables

Different variables are available depending on where the expression is used.

### Request Variables

Available in: Path, Headers, Query, Request Body

| Variable             | Type   | Description                                |
| -------------------- | ------ | ------------------------------------------ |
| `request.method`     | string | HTTP method (GET, POST, etc.)              |
| `request.scheme`     | string | Protocol (http or https)                   |
| `request.host`       | string | Request hostname                           |
| `request.port`       | int    | Server port                                |
| `request.path`       | string | Request path (after API slug)              |
| `request.url`        | string | Full request URL                           |
| `request.ip`         | string | Client IP address                          |
| `request.country`    | string | Client country code                        |
| `request.body`       | string | Raw request body                           |
| `request.json`       | object | Parsed JSON body (if valid JSON)           |
| `request.headers`    | object | Request headers (single value per key)     |
| `request.headersAll` | object | Request headers (array of values per key)  |
| `request.query`      | object | Query parameters (single value per key)    |
| `request.queryAll`   | object | Query parameters (array of values per key) |
| `request.raw.path`   | string | Raw path before transformation             |
| `request.raw.query`  | string | Raw query string                           |

### Context Variables

Available in: All expressions

| Variable                  | Type      | Description                                                                    |
| ------------------------- | --------- | ------------------------------------------------------------------------------ |
| `context.userId`          | string    | Client user identifier                                                         |
| `context.username`        | string    | Client username (users can change it)                                          |
| `context.planId`          | string    | Client's subscription plan slug                                                |
| `context.planType`        | string    | Plan type (`periodic` or `payasyougo`)                                         |
| `context.endpointCredits` | int\|null | Credits defined for this endpoint (`null` for endpoints with flexible credits) |

### Response Variables

Available in: Response Body, Response Headers, Dynamic Credits

| Variable              | Type   | Description                                |
| --------------------- | ------ | ------------------------------------------ |
| `response.status`     | int    | HTTP status code from target               |
| `response.body`       | string | Raw response body                          |
| `response.json`       | object | Parsed JSON response (if valid JSON)       |
| `response.headers`    | object | Response headers (single value per key)    |
| `response.headersAll` | object | Response headers (array of values per key) |
| `response.latency`    | float  | Request latency in seconds                 |

### Rendered Request Variables

Available in: Response Body, Response Headers, Dynamic Credits

The `renderedRequest` object contains the transformed request that was actually sent to the target server.

| Variable                     | Type   | Description                  |
| ---------------------------- | ------ | ---------------------------- |
| `renderedRequest.method`     | string | Final HTTP method sent       |
| `renderedRequest.url`        | string | Final URL sent to target     |
| `renderedRequest.host`       | string | Target hostname              |
| `renderedRequest.path`       | string | Transformed path             |
| `renderedRequest.body`       | string | Transformed request body     |
| `renderedRequest.json`       | object | Parsed transformed body      |
| `renderedRequest.headers`    | object | Headers sent to target       |
| `renderedRequest.headersAll` | object | Headers sent (array per key) |

## Accessing Properties

Use dot notation for object properties:

```
{{ request.json }}
{{ response.headers }}
```

Use bracket notation for dynamic keys or special characters:

```
{{ request.headers["x-custom-header"] }}
{{ response.json["data"][0] }}
```

## Functions

### String Functions

| Function                      | Description                        | Example                                       |
| ----------------------------- | ---------------------------------- | --------------------------------------------- |
| `lower(str)`                  | Convert to lowercase               | `{{ lower(request.headers["accept"]) }}`      |
| `upper(str)`                  | Convert to uppercase               | `{{ upper(request.method) }}`                 |
| `trim(str)`                   | Remove leading/trailing whitespace | `{{ trim(request.query["name"]) }}`           |
| `concat(a, b)`                | Concatenate two strings            | `{{ concat("/v2", request.path) }}`           |
| `substring(str, start, len?)` | Extract substring                  | `{{ substring(request.path, 0, 10) }}`        |
| `replace(str, from, to)`      | Replace occurrences                | `{{ replace(request.path, "/v1", "/v2") }}`   |
| `split(str, delim)`           | Split into array                   | `{{ split(request.query["ids"], ",") }}`      |
| `join(arr, delim)`            | Join array elements                | `{{ join(request.json["tags"], ",") }}`       |
| `startsWith(str, prefix)`     | Check prefix                       | `{{ startsWith(request.path, "/api") }}`      |
| `endsWith(str, suffix)`       | Check suffix                       | `{{ endsWith(request.path, ".json") }}`       |
| `contains(str, needle)`       | Check if contains                  | `{{ contains(request.body, "error") }}`       |
| `padLeft(str, len, char?)`    | Pad start                          | `{{ padLeft(request.query["id"], 10, "0") }}` |
| `padRight(str, len, char?)`   | Pad end                            | `{{ padRight(request.query["code"], 5) }}`    |

### Array Functions

| Function                  | Description       | Example                                     |
| ------------------------- | ----------------- | ------------------------------------------- |
| `first(arr)`              | Get first element | `{{ first(response.json["results"]) }}`     |
| `last(arr)`               | Get last element  | `{{ last(response.json["items"]) }}`        |
| `slice(arr, start, len?)` | Extract portion   | `{{ slice(response.json["data"], 0, 5) }}`  |
| `keys(obj)`               | Get object keys   | `{{ keys(request.headers) }}`               |
| `values(obj)`             | Get object values | `{{ values(request.query) }}`               |
| `in(needle, arr)`         | Check membership  | `{{ in(request.method, ["GET", "POST"]) }}` |
| `merge(arr1, arr2)`       | Merge arrays      | `{{ merge(arr1, arr2) }}`                   |
| `len(arr)`                | Get length        | `{{ len(response.json["results"]) }}`       |

### Encoding Functions

| Function            | Description   | Example                                       |
| ------------------- | ------------- | --------------------------------------------- |
| `urlEncode(str)`    | URL encode    | `{{ urlEncode(request.query["callback"] }}`   |
| `base64Encode(str)` | Base64 encode | `{{ base64Encode(request.body) }}`            |
| `base64Decode(str)` | Base64 decode | `{{ base64Decode(request.headers["auth"]) }}` |
| `jsonEncode(val)`   | JSON encode   | `{{ jsonEncode(request.json["data"]) }}`      |
| `jsonDecode(str)`   | JSON decode   | `{{ jsonDecode(request.body) }}`              |

### Type Functions

| Function        | Description        | Example                                 |
| --------------- | ------------------ | --------------------------------------- |
| `toInt(val)`    | Convert to integer | `{{ toInt(request.query["page"]) }}`    |
| `toFloat(val)`  | Convert to float   | `{{ toFloat(request.query["price"]) }}` |
| `isString(val)` | Check if string    | `{{ isString(request.body) }}`          |
| `isNumber(val)` | Check if number    | `{{ isNumber(request.json["count"]) }}` |
| `isArray(val)`  | Check if array     | `{{ isArray(response.json["items"]) }}` |
| `isBool(val)`   | Check if boolean   | `{{ isBool(request.json["active"]) }}`  |

### Utility Functions

| Function                 | Description            | Example                                                  |
| ------------------------ | ---------------------- | -------------------------------------------------------- |
| `empty(val)`             | Check if null/empty    | `{{ empty(request.query["filter"]) }}`                   |
| `exists(val)`            | Check if not null      | `{{ exists(request.json["user"]) }}`                     |
| `default(val, fallback)` | Default if empty       | `{{ default(request.query["page"], "1") }}`              |
| `coalesce(a, b, ...)`    | First non-empty        | `{{ coalesce(request.json["id"], request.query["id"] }}` |
| `min(a, b)`              | Minimum value          | `{{ min(request.json["count"], 100) }}`                  |
| `max(a, b)`              | Maximum value          | `{{ max(request.json["page"], 1) }}`                     |
| `abs(val)`               | Absolute value         | `{{ abs(request.json["offset"]) }}`                      |
| `round(val, precision?)` | Round number           | `{{ round(response.json["price"], 2) }}`                 |
| `floor(val)`             | Round down             | `{{ floor(response.json["rating"]) }}`                   |
| `ceil(val)`              | Round up               | `{{ ceil(response.json["pages"]) }}`                     |
| `buildQuery(params)`     | Build URL query string | `{{ buildQuery({"page":1,"limit":10}) }}`                |

### Hash & Random Functions

| Function             | Description      | Example                              |
| -------------------- | ---------------- | ------------------------------------ |
| `hash(algo, str)`    | Hash string      | `{{ hash("sha256", request.body) }}` |
| `uuid()`             | Generate UUID v4 | `{{ uuid() }}`                       |
| `random(min?, max?)` | Random integer   | `{{ random(1, 100) }}`               |

Supported hash algorithms: `md5`, `sha1`, `sha256`, `sha512`

### Date Functions

| Function                   | Description            | Example                     |
| -------------------------- | ---------------------- | --------------------------- |
| `now()`                    | Current Unix timestamp | `{{ now() }}`               |
| `date(format, timestamp?)` | Format date            | `{{ date("Y-m-d H:i:s") }}` |

### URL Functions

| Function                   | Description    | Example                          |
| -------------------------- | -------------- | -------------------------------- |
| `parseUrl(url, component)` | Parse URL part | `{{ parseUrl(request.url, 1) }}` |

Component values: 1=scheme, 2=host, 3=port, 5=path, 6=query, 7=fragment

## Operators

### Comparison

```
{{ request.json["count"] > 10 }}
{{ request.method == "POST" }}
{{ response.status != 200 }}
{{ request.json["price"] >= 100 }}
```

### Logical

```
{{ request.json["active"] && request.json["verified"] }}
{{ request.query["format"] == "xml" || request.query["format"] == "json" }}
{{ !empty(request.json["email"]) }}
```

### Ternary

```
{{ request.json["premium"] ? "pro" : "free" }}
{{ empty(request.query["limit"]) ? 10 : toInt(request.query["limit"]) }}
```

### String Concatenation

```
{{ "/api/v2" ~ request.path }}
{{ request.json.["first"] ~ " " ~ request.json["last"] }}
```

### Arithmetic

```
{{ request.json["price"] * 1.1 }}
{{ response.json["total"] / response.json["count"] }}
{{ request.json["page"] + 1 }}
```

## Usage Examples

### Path Transformation

Prefix all requests with a version:

```
/v2{{ request.path }}
```

Dynamic path based on request:

```
/users/{{ request.json["user_id"] }}/profile
```

### Custom Headers

Add authentication header:

```
Bearer {{ base64Encode(context.userId ~ ":" ~ context.planId) }}
```

Forward user context:

```
{{ context.userId }}
```

### Query Parameters

Default pagination:

```
{{ default(request.query["limit"], "20") }}
```

### Request Body Transformation

Wrap original body:

```json theme={null}
{
  "data": {{ request.body }},
  "metadata": {
    "user": "{{ context.userId }}",
    "timestamp": {{ now() }}
  }
}
```

### Response Body Transformation

Extract specific fields:

```json theme={null}
{
  "items": {{ jsonEncode(response.json["data"]["results"]) }},
  "total": {{ response.json["data"]["total_count"] }}
}
```

### Dynamic Credits

Use credits from response header:

```
{{ response.headers["x-credits-used"] }}
```

Calculate credits based on response:

```
{{ len(response.json["results"]) }}
```

Fixed credits on success, zero on error:

```
{{ response.status >= 200 && response.status < 300 ? context.endpointCredits : 0 }}
```

## Limits

* Maximum template size: 200,000 characters
* Maximum expressions per template: 100

## Notes

* Expressions that fail to evaluate will throw an error and the request will return a gateway error
* JSON objects and arrays in expression results are automatically serialized
* The `request.json` and `response.json` variables are `null` if the body is not valid JSON
* Header names are case-insensitive and normalized to lowercase
