> Fetch the complete documentation index at: https://wasp.sh/llms.txt
---

# Custom HTTP API Endpoints

In Wasp, the default client-server interaction mechanism is through [Operations](https://wasp.sh/docs/data-model/operations/overview). However, if you need a specific URL method/path, or a specific response, Operations may not be suitable for you. For these cases, you can use an `api`. Best of all, they should look and feel very familiar.

## How to Create an API

APIs are used to tie a JS function to a certain endpoint e.g. `POST /something/special`. They are distinct from Operations and have no client-side helpers (like `useQuery`).

To create a Wasp API, you must:

1. Declare the API in Wasp using the `api` constructor
2. Define the API's NodeJS implementation

After completing these two steps, you'll be able to call the API from the client code (via our `ky` wrapper), or from the outside world.

### Specifying the API in Wasp

First, we need to declare the API in the Wasp file and you can easily do this with the `api` function:

```ts title="main.wasp.ts"
import { api, app } from "@wasp.sh/spec"
import { fooBar } from "./src/apis" with { type: "ref" }

export default app({
  // ...
  spec: [
    api("GET", "/foo/bar", fooBar),
  ],
})
```

:::note
When `main.wasp.ts` needs to point to your code, it uses imports like this:

```ts
import { MainPage } from "./src/MainPage" with { type: "ref" }
```

Notice the `with { type: "ref" }` part at the end of the import statement. This tells Wasp to treat the import as a reference to your app's code, without running the imported code. For more details and examples, see [reference imports](https://wasp.sh/docs/general/spec#reference-imports).
:::

Read more about the supported fields in the [API Reference](#api-reference).

### Defining the API's NodeJS Implementation

:::note
To make sure the Wasp compiler generates the types for APIs for use in the NodeJS implementation, you should add your `api`s to your Wasp file first *and* keep the `wasp start` command running.
:::

After you defined the API, it should be implemented as a NodeJS function that takes three arguments:

1. `req`: Express Request object
2. `res`: Express Response object
3. `context`: An additional context object **injected into the API by Wasp**. This object contains user session information, as well as information about entities. The examples here won't use the context for simplicity purposes. You can read more about it in the [section about using entities in APIs](#using-entities-in-apis).

```ts title="src/apis.ts"
import type { FooBar } from "wasp/server/api";

export const fooBar: FooBar = (req, res, context) => {
  res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
  res.json({ msg: `Hello, ${context.user ? "registered user" : "stranger"}!` });
};
```

:::note
The `FooBar` type is generated by Wasp based on the `api` spec above.
:::

#### Providing Extra Type Information

We'll see how we can provide extra type information to an API function.

Let's say you wanted to create some `GET` route that would take an email address as a param, and provide them the answer to "Life, the Universe and Everything." 😀 What would this look like in TypeScript?

Define the API in Wasp:

```ts title="main.wasp.ts"
import { api, app } from "@wasp.sh/spec"
import { fooBar } from "./src/apis" with { type: "ref" }

export default app({
  // ...
  spec: [
    api("GET", "/foo/bar/:email", fooBar, { entities: ["Task"] }),
  ],
})
```

We can use the `FooBar` type to which we'll provide the generic **params** and **response** types, which then gives us full type safety in the implementation.

```ts title="src/apis.ts"
import { FooBar } from "wasp/server/api";

export const fooBar: FooBar<
  { email: string }, // params
  { answer: number } // response
> = (req, res, _context) => {
  console.log(req.params.email);
  res.json({ answer: 42 });
};
```

## Using the API

### Using the API externally

To use the API externally, you simply call the endpoint using the method and path you used.

For example, if your app is running at `https://example.com` then from the above you could issue a `GET` to `https://example/com/foo/callback` (in your browser, Postman, `curl`, another web service, etc.).

### Using the API from the Client

To use the API from your client, including with auth support, you can import the `api` instance from `wasp/client/api`. It is a [ky](https://github.com/sindresorhus/ky) instance pre-configured with the API base URL, authentication, and error handling. For example:

```tsx title="src/pages/SomePage.tsx"
import React, { useEffect } from "react";
import { api } from "wasp/client/api";

async function fetchCustomRoute() {
  const data = await api.get("/foo/bar").json();
  console.log(data);
}

export const Foo = () => {
  useEffect(() => {
    fetchCustomRoute();
  }, []);

  return <>{/* ... */}</>;
};
```

#### Making Sure CORS Works

APIs are designed to be as flexible as possible, hence they don't utilize the default middleware like Operations do. As a result, to use these APIs on the client side, you must ensure that CORS (Cross-Origin Resource Sharing) is enabled.

You can do this by defining custom middleware for your APIs in the Wasp file.

For example, an `apiNamespace` is a simple spec used to apply some `middlewareConfigFn` to all APIs under some specific path:

```ts title="main.wasp.ts"
import { apiNamespace, app } from "@wasp.sh/spec"
import { apiMiddleware } from "./src/apis" with { type: "ref" }

export default app({
  // ...
  spec: [
    apiNamespace("/foo", { middlewareConfigFn: apiMiddleware }),
  ],
})
```

And then in the implementation file (returning the default config):

```ts title="src/apis.ts"
import type { MiddlewareConfigFn } from "wasp/server";
export const apiMiddleware: MiddlewareConfigFn = (config) => {
  return config;
};
```

We are returning the default middleware which enables CORS for all APIs under the `/foo` path.

For more information about middleware configuration, please see: [Middleware Configuration](https://wasp.sh/docs/advanced/middleware-config)

## Using Entities in APIs

In many cases, resources used in APIs will be [Entities](https://wasp.sh/docs/data-model/entities). To use an Entity in your API, add it to the `api` spec in Wasp:

```ts title="main.wasp.ts"
import { api, app } from "@wasp.sh/spec"
import { fooBar } from "./src/apis" with { type: "ref" }

export default app({
  // ...
  spec: [
    api("GET", "/foo/bar", fooBar, { entities: ["Task"] }),
  ],
})
```

Wasp will inject the specified Entity into the APIs `context` argument, giving you access to the Entity's Prisma API:

```ts title="src/apis.ts"
import type { FooBar } from "wasp/server/api";

export const fooBar: FooBar = async (req, res, context) => {
  res.json({ count: await context.entities.Task.count() });
};
```

The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud).

## Streaming Responses

You can use streaming responses to send data to the client in chunks as it becomes available. This is useful for:

- **LLM responses** - Stream AI-generated content as it's produced
- **Long-running processes** - Show progress updates in real-time
- **Large datasets** - Send data incrementally to avoid timeouts

### Creating a Streaming API

To create a streaming API, write a function that uses Express response methods like `res.write()` and `res.end()`:

```ts title="main.wasp.ts"
import { api, app } from "@wasp.sh/spec"
import { getStreamingText } from "./src/streaming" with { type: "ref" }

export default app({
  // ...
  spec: [
    api("POST", "/api/streaming-example", getStreamingText),
  ],
})
```

Don't forget to set up the CORS middleware. See the [section explaning CORS](#making-sure-cors-works) for details.

```ts title="src/streaming.ts"
import OpenAI from "openai";
import type { GetStreamingText } from "wasp/server/api";

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export const getStreamingText: GetStreamingText<
  never,
  string,
  { message: string }
> = async (req, res) => {
  const { message } = req.body;

  // Set appropriate headers for streaming.
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
  res.setHeader("Transfer-Encoding", "chunked");

  const stream = await client.responses.create({
    model: "gpt-5",
    input: `Funny response to "${message}"`,
    stream: true,
  });

  for await (const chunk of stream) {
    if (chunk.type === "response.output_text.delta") {
      // Write each chunk to the response as it arrives.
      res.write(chunk.delta);
    }
  }

  // End the response.
  res.end();
};
```

### Consuming Streaming Responses

You can consume streaming responses on the client using the `api` instance from `wasp/client/api`. Since ky is built on `fetch`, you get native streaming support via the `Response.body` readable stream. The `api` instance handles authentication automatically.

```tsx title="src/StreamingPage.tsx"
import { useEffect, useState } from "react";
import { api } from "wasp/client/api";

export function StreamingPage() {
  const { response } = useTextStream("/api/streaming-example", {
    message: "Best Office episode?",
  });

  return (
    <div>
      <h1>Streaming Example</h1>
      <pre>{response}</pre>
    </div>
  );
}

function useTextStream(path: string, payload: { message: string }) {
  const [response, setResponse] = useState("");

  useEffect(() => {
    const controller = new AbortController();

    fetchStream(
      path,
      payload,
      (chunk) => {
        setResponse((prev) => prev + chunk);
      },
      controller.signal,
    );

    return () => {
      controller.abort();
    };
  }, [path]);

  return { response };
}

async function fetchStream(
  path: string,
  payload: { message: string },
  onData: (data: string) => void,
  signal: AbortSignal,
) {
  try {
    const response = await api.post(path, {
      json: payload,
      signal,
    });

    if (response.body === null) {
      throw new Error("Stream body is null");
    }

    const stream = response.body.pipeThrough(new TextDecoderStream());
    const reader = stream.getReader();
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        break;
      }
      onData(value);
    }
  } catch (error: unknown) {
    if (error instanceof Error) {
      if (error.name === "AbortError") {
        // Fetch was aborted, no need to log an error
        return;
      }
      console.error("Fetch error:", error.message);
    } else {
      throw error;
    }
  }
}
```

## API Reference

[API reference](https://wasp.sh/docs/api/@wasp.sh/spec/functions/api)

### [api »](https://wasp.sh/docs/api/@wasp.sh/spec/functions/api)

[All the options for declaring an API endpoint in the Wasp spec.](https://wasp.sh/docs/api/@wasp.sh/spec/functions/api)

[API reference](https://wasp.sh/docs/api/@wasp.sh/spec/functions/apiNamespace)

### [apiNamespace »](https://wasp.sh/docs/api/@wasp.sh/spec/functions/apiNamespace)

[All the options for declaring an API namespace in the Wasp spec.](https://wasp.sh/docs/api/@wasp.sh/spec/functions/apiNamespace)