How to make a chatbot for the Reddit API
Tags: ai, nextjs, openapi, tutorial
Published: 2023-09-13 07:27:28

Hi there! Thought I’d share this short guide on how I made a chatbot that lets users converse with the Reddit API. As a dev and Reddit fan, I wanted to try creating a new way of interacting with the platform. Here's how I did it using NextJS, Chatwith and OpenAPI! 

> [Here is the source code](https://github.com/rafalzawadzki/redditbot) if you want to jump right into it, and here is the final app: [https://redditbot.chat](https://redditbot.chat)

## Project features

In summary, the project has:

- A thin wrapper over the [Reddit API](https://www.reddit.com/dev/api/) to expose only useful endpoints and trim data payloads
- An OpenAPI spec file in YAML for the [Chatwith](https://chatwith.tools) chatbot
- A landing page with a chatbot iframe embed

Arguably most of the heavy lifting has been done by Chatwith. This project focuses on creating an API for the chatbot to interact with and setting up Chatwith to build the chatbot.

I used NextJS for this project because I also wanted to make a simple landing page - but you can easily create the API with eg. an Express backend.

## Getting started

I used the [plain NextJS 13 starter](https://nextjs.org/docs/pages/api-reference/create-next-app) with the default configuration:

`bunx create-next-app`

You can see I'm using [Bun](https://bun.sh) which is the new JS all-in-one toolkit. You can also use `npm` or `pnpm` as a replacement in this project.

After creating the project, quickly check if it runs on localhost:

`bun dev`

## Creating Reddit API wrapper

The essential component of this project is the Reddit API wrapper. It serves as a mediator between our chatbot and the Reddit API. Its role is to reduce the amount of data transferred and to simplify the API for the chatbot to understand.

Here's why the wrapper is important:

- **Use only selected endpoints**: The Reddit API has a myriad of endpoints for various functionalities. Our chatbot only needs a subset of these. The wrapper makes sure to expose only the necessary ones.
- **Payload size**: Reddit API responses contain a ton of information that we don't need. Because GPT-3.5 and GPT-4 from OpenAI accept limited tokens, the wrapper trims the API responses to contain only the data the bot will actually use.

### API endpoints

Check the official [Reddit API documentation](https://www.reddit.com/dev/api/) for the full specification of all endpoints.

For now we'll add the following Reddit API endpoints in our wrapper:

- `/r/{subreddit}/{listing}.json`: Fetches stories from a specific subreddit based on the type of listing (hot, new, rising, etc.)
- `/{listing}.json`: Fetches stories from all subreddits based on the type of listing.
- `/r/{subreddit}/search.json`: Allows us to search for a topic within any subreddit.
- `/search.json`: Enables a search across all subreddits.
- `/r/{subreddit}/comments/{post_id}.json`: Retrieves comments from a specific post within a subreddit.
 
### Create API endpoint

Let's create a new NextJS route handler which will:

- relay requests to Reddit API
- trim the payloads and return a response

First, create a new file: `/app/api/[...catchall]/route.ts`

> `[...catchall]` is the name of the directory and means that any path at `/api/` will be handled by this route (so `/api/search`, `/api/anything/goes` etc). Learn more [here](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes#catch-all-segments).

In this file is a GET handler:


```typescript
export const runtime = "edge";
export const revalidate = 10;

export async function GET(request: Request) {
  const urlObj = new URL(request.url);
  const path = urlObj.pathname.replace("/api/", "");
  const queryParams = urlObj.search;

  try {
    const redditResponse = await fetch(
      `https://api.reddit.com/${path}${queryParams}`
    );
    const redditData = await redditResponse.json();

    if (isPostListingResponse(redditData)) {
      // Handle posts
      const summaries = redditData.data.children.map(extractPostSummary);
      return NextResponse.json(summaries);
    } else if (isCommentsResponse(redditData)) {
      if (path.startsWith("r/") && path.includes("/comments/")) {
        // Handle comments
        const comments = redditData[1].data.children.map(extractCommentSummary);
        return NextResponse.json(comments);
      }
    } else {
      return new Response(null, {
        status: 400,
        statusText: "Invalid Reddit API response",
      });
    }
  } catch (error) {
    console.error(error);
    return new Response(null, {
      status: 500,
      statusText: "Failed to fetch data from Reddit",
    });
  }
}
```

Let break it down step by step!

We'll use the Edge runtime for lowest latency and add short caching to not get rate limited by the Reddit API:

```ts
export const runtime = "edge";
export const revalidate = 10;
```

Then, we clean up the request path so we can switch the host from our API to Reddit one:

```ts
const urlObj = new URL(request.url);
const path = urlObj.pathname.replace("/api/", "");
const queryParams = urlObj.search;
```

Next, we make a request to Reddit API and parse the response JSON:

```ts
const redditResponse = await fetch(`https://api.reddit.com/${path}${queryParams}`);
const redditData = await redditResponse.json();
```

Then we fiddle with data structures to recognize what kind of response we're dealing with so we can trim it and return back to the client:

```ts
if (isPostListingResponse(redditData)) {
  // Handle posts
  const summaries = redditData.data.children.map(extractPostSummary);
  return NextResponse.json(summaries);
} else if (isCommentsResponse(redditData)) {
  if (path.startsWith("r/") && path.includes("/comments/")) {
    // Handle comments
    const comments = redditData[1].data.children.map(extractCommentSummary);
    return NextResponse.json(comments);
  }
} else {
  return new Response(null, {
    status: 400,
    statusText: "Invalid Reddit API response",
  });
}
```

You can see the full source code for the `isPostListingResponse`, `isCommentsResponse`, `extractPostSummary` and `extractCommentSummary` functions in the repository. The first two functions just check the existence and types of JSON elements, and the latter two map large objects returned by Reddit API into simplified ones that contain post title, author name etc.

## OpenAPI specification

To allow our chatbot to talk to our API, we'll need to describe it using the [OpenAPI specification](https://spec.openapis.org/oas/latest.html) (as per Chatwith requirements).

Let's start by creating a YAML file in the static assets directory: `/public/openapi.yml`. This way the file will be accessible to Chatwith after deployment like this: [http://redditbot.chat/openapi.yml](http://redditbot.chat/openapi.yml)

Here's what we'll put in the file:

```yml
openapi: 3.0.0
info:
  title: Simple Reddit API
  version: 1.0.0
  description: An API specification for a simple Reddit client. Using this API you can respond to user queries by searching any subreddit or listing type.
servers:
  - url: https://redditbot.chat/api/
paths:
  /r/{subreddit}/{listing}.json:
    get:
      summary: Fetch stories from a specific subreddit by listing type
      parameters:
        - name: subreddit
          in: path
          required: true
          schema:
            type: string
        - name: listing
          in: path
          required: true
          schema:
            type: string
            enum:
              - hot
              - new
              - rising
              - top
              - controversial
        - name: limit
          in: query
          schema:
            type: integer
      responses:
        "200":
          description: A JSON object containing stories

[... trimmed for brevity]
```

> You can see the whole specification file content [here](https://github.com/rafalzawadzki/redditbot/blob/main/public/openapi.yml). Make sure to stick to YAML whitespace formatting! Wrong tabs or spaces here and there may result in an incorrect spec file.

### The important bits

Make sure to put the final path to your API route after its deployed. In my case, the project is deployed at `https://redditbot.chat` so the path to API wrapper is `https://redditbot.chat/api/`:

```yml
servers:
  - url: https://redditbot.chat/api/
```

Document all endpoints the chatbot can use, along with short description and parameters.

```yml
paths:
  /r/{subreddit}/{listing}.json:
...
```

> The OpenAPI specification does not need to be very extensive and detailed, but it must be descriptive enough for the chatbot to understand how to make a correct request.

At this point we are over the hump, the hardest part is behind! Let's test the new endpoints by calling in the browser eg.:

`http://localhost:3000/api/search.json?q=cat`

This should return a JSON response with a bunch of cat posts! 😻

## Creating the chatbot

The final step is creating a chatbot in Chatwith to get an embed. I'm using the free account. I followed [this guide](https://chatwith.tools/help/skills/rest-api-skills-integration-make-your-chatbot-do-stuff). Here are the steps:

### Main steps

1. Create a free Chatwith account [here](https://chatwith.tools)
2. During onboarding pick the **'Chat with API'** option
3. Click **'Install'** on the **'API'** card
4. Enter our API details in the popup
5. Finish the onboarding flow and go to the dashboard to add a system prompt and customize the chatbot

### Installing the API Action

Here's the screenshot of how installing the action popup looks like. You should put your OpenAPI spec file url and API urls (but not the localhost! must be the public url after deployment on eg Vercel).


![Screenshot of Chatwith popup for adding API action](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/36nmmlak9ph2zrwi7fqg.png)

### Adding system prompt

To give the chatbot instructions how to use the API, we will fill in the **'System Prompt'** field in the **'Settings'** section in the dashboard.

Here is the prompt:

> You are Reddit AI assistant. You will respond to user query and using the provided API search posts in any subreddit and also by categories like hot, newest etc. You will provide a summary of listings and comments in an easy & digestible way. You are flexible and can understand the query so users can ask about news, fun posts, specialized subreddits etc. You will take the user query and try to make the best API request to find an answer to it. If a post has an image attached, you will show a thumbnail in markdown. Always provide post permalinks in the answer.

You can extend the prompt to add more character to the chatbot, eg. make it use emojis or funny language 😸

### Testing the chatbot

We're almost done! The chatbot should be working now. Let's go to **'Preview'** section of the dashboard to test the chatbot. You can start by asking a question like `Summarize hottest posts`!

## Fin

Hopefully at this point you have a cool chatbot that can talk to an API 👏 If something didn't work or you have questions feel free to ping me in the comments!

As next steps, you can add more actions to your chatbot (ie. add more API endpoints for it to use) and customize how it looks in the Chatwith dashboard. After updating the API make sure to save the action in Chatwith again so the chatbot knows about the changes.

Thanks for reading!