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

# Writing a function

> The handler contract, what you receive and what you can return, how several functions chain, what happens on errors, and the runtime limits

A function is a folder under `functions/` in your project with an `index.ts` that exports a **named `handler`**:

```
functions/
└── block-po-boxes/
    ├── index.ts
    └── function.json     # local-only wiring for `ollieshop function dev`
```

```typescript functions/block-po-boxes/index.ts theme={"system"}
export const handler = async ({ req, res }: { req: Request; res: Response | null }) => {
  // request function: inspect or rewrite `req`, or answer with a Response
  // response function: inspect or rewrite `res`
  return undefined; // nothing changes
};
```

The export must be named `handler`. A default export is ignored.

## What you receive

| | Request function | Response function |
| - | - | - |
| `req` | The request the checkout server is about to send to the platform | The request that was sent |
| `res` | `null` | The platform's response |

Both are standard `Request` / `Response` objects with one improvement: **their bodies can be read more than once**. `await req.json()` in one function and again in the next is fine. Two things to remember:

* A `GET` request has no body, so `req.json()` throws on it. Check `req.method` first.
* `formData()` is not supported; read `text()` and parse it yourself.

The URL of `req` is the absolute platform URL, for example `https://acme.vtexcommercestable.com.br/api/checkout/pub/orderForm/<id>/items`.

## What you can return

| Return | Request function | Response function |
| - | - | - |
| `undefined` | Send the request unchanged | Pass the response unchanged |
| `new Request(...)` | **Send this instead.** You can change the body, headers and URL | Ignored |
| `new Response(...)` | **Answer with this**, skipping the platform and every remaining function | Pass this response to the next function and, eventually, to the checkout |

Anything else (`null`, an object, a string) is an error: `Custom function must return a Request, a Response, or undefined`.

### Rewriting a request

```typescript theme={"system"}
export const handler = async ({ req }: { req: Request; res: Response | null }) => {
  if (req.method !== "POST") return;

  const body = await req.json();
  body.marketingData = { ...body.marketingData, marketingTags: ["from-ollie"] };

  return new Request(req.url, {
    method: req.method,
    headers: req.headers,
    body: JSON.stringify(body),
  });
};
```

### Enriching a response

The checkout parses the platform's response into the session. Fields you add under `extensions` surface as `session.extensions` in [`useCheckoutSession`](/ollie-shop/api/useCheckoutSession), which is how a function hands data to a component:

```typescript theme={"system"}
export const handler = async ({ res }: { req: Request; res: Response | null }) => {
  if (!res) return;

  const orderForm = await res.json();
  const hasWarrantyItem = orderForm.items.some((item: { productCategoryIds: string }) =>
    item.productCategoryIds.includes("/6/"),
  );

  return new Response(
    JSON.stringify({ ...orderForm, extensions: { checkoutSteps: { warranty: { isCompleted: !hasWarrantyItem } } } }),
    { status: res.status, headers: res.headers },
  );
};
```

### Rejecting an action with a message

A request function that returns a `Response` stops the call. To show the shopper a message instead of a generic error, answer with a 4xx status and the `x-ollie-shop-custom-message` header; the checkout displays that text:

```typescript theme={"system"}
export const handler = async ({ req }: { req: Request; res: Response | null }) => {
  const body = await req.json();
  const street: string = body.address?.street ?? "";

  if (/p\.?o\.? box/i.test(street)) {
    return new Response(JSON.stringify({ message: "PO boxes are not accepted" }), {
      status: 422,
      headers: {
        "content-type": "application/json",
        "x-ollie-shop-custom-message": "We cannot deliver to PO boxes. Please use a street address.",
      },
    });
  }
};
```

Use a 4xx status for rejections. The checkout may retry 5xx responses, which would run your function again.

## Several functions on one call

* All matching **request** functions run first, then the platform is called once, then all matching **response** functions run.
* Within each kind, functions run in **priority** order: **lowest number first**, ties by creation date. Priority never reorders a request function after a response function.
* Each function receives the output of the previous one: a request rewritten by priority 0 is what priority 1 sees.
* A request function that returns a `Response` ends the chain: the remaining request functions, the platform call and the response functions are skipped.

## Errors

A function fails when it throws, returns an invalid value, exceeds its time limit, or its trigger expression cannot be evaluated. What happens next is the function's **Error Behavior**:

| Setting | Effect |
| - | - |
| **Skip** | The failure is logged, the function's output (and its session storage writes) is discarded, and the call continues as if the function had returned nothing. Default for functions created in the Admin |
| **Throw** | The call fails. The checkout receives a 502, the shopper sees "An unexpected error occurred. Please try again later or contact support.", and the checkout may retry the call. Default for functions created with the CLI |

Use **Throw** only for rules that must block the action. For anything else, catch your own errors and use **Skip**, so a third-party outage cannot break checkout.

## Environment variables and secrets

The store's [Environment Variables](/ollie-shop/configuration/env-vars.md) are available as `process.env.MY_KEY` **inside the handler**. They are injected per call, so read them in the handler, not at the top of the module:

```typescript theme={"system"}
export const handler = async ({ req }: { req: Request; res: Response | null }) => {
  const apiKey = process.env.FRAUD_API_KEY; // inside the handler
  // ...
};
```

Values are used exactly as saved; trim them in the Admin rather than in code. Every function of the store receives the same variables.

## Logging

`console.log`, `console.warn` and `console.error` are captured as structured logs with the function id and store id. Function logs are available on request from Ollie support. Keep lines short and avoid logging full payloads.

## Runtime and limits

| | |
| - | - |
| Runtime | Node.js 22, 1 GB of memory |
| Time limit | **5 seconds** per invocation, including your outbound calls. A timed-out function counts as an error |
| Payload | The request and response bodies, the session and the environment are passed to the function on every call |
| Dependencies | Everything in your `package.json` dependencies is bundled into the function. Native modules and files read at runtime do not work |
| Upload | 10 MB zip |
| Concurrency | Each call to a function is independent; module-level variables are not shared reliably between calls |

> [!WARNING]
>
> The runtime settings of a function (Node version, memory, time limit and runtime library) are fixed when the function is **first deployed**. Redeploying code does not update them. If a function was created long ago and misses a capability described here, create a new function and deploy to it.

## Types

The platform package that wraps your handler is not published, so declare the context type yourself or copy this one:

```typescript functions/types.ts theme={"system"}
export type FunctionContext = { req: Request; res: Response | null };
export type FunctionHandler = (ctx: FunctionContext) => Promise<Request | Response | undefined>;
```

## Related

* [Triggers](/ollie-shop/functions/triggers.md): choosing which calls run the function
* [Session storage and cookies](/ollie-shop/functions/session-storage-and-cookies.md): state across calls
* [function dev](/ollie-shop/cli/function-dev.md): running it locally
* [Deploying](/ollie-shop/functions/deploying.md): the record, the code and activation


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.