---
title: "Add llms.txt"
description: "Implement the llms.txt standard for the feedback API, add llms-full.txt for single-request access, and add markdown docs so agents can discover and read your API documentation in machine-readable formats."
canonical_url: "https://vercel.com/academy/agent-friendly-apis/add-llms-txt"
md_url: "https://vercel.com/academy/agent-friendly-apis/add-llms-txt.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T12:42:54.250Z"
content_type: "lesson"
course: "agent-friendly-apis"
course_title: "Agent-Friendly APIs"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Add llms.txt

# Add llms.txt and Markdown Access

Most restaurants have a menu posted outside the door. You can stand on the sidewalk and know exactly what they serve, what the prices are, and whether they have that one dish you came for. No need to awkwardly walk in, sit down, and ask the waiter to list everything. The menu is the invitation. It tells you: here's what we've got, here's how to get it.

Your API needs one of those menus.

Right now, even if you had perfect docs at `/api/docs`, an agent wouldn't know that endpoint exists unless someone told it. There's no standard place to look, no convention to follow. The agent has to guess, or the developer has to hardcode the URL into every prompt.

The `llms.txt` standard fixes this. It's a single file at a well-known path that tells agents what your API offers and where to find the details.

## Outcome

Add `/llms.txt`, `/llms-full.txt`, and `/api/docs.md` routes to the feedback API so agents can discover, skim, and read your documentation in machine-readable formats.

## Fast Track

1. Fill in the llms.txt content in `app/llms.txt/route.ts` (stub provided in starter)
2. Fill in the complete docs in `app/llms-full.txt/route.ts` (stub provided in starter)
3. Fill in the markdown docs in `app/api/docs.md/route.ts` (stub provided in starter)
4. Verify all three endpoints return the correct content types

## The llms.txt standard

The `llms.txt` spec (from [llmstxt.org](https://llmstxt.org)) defines a simple markdown format that lives at the root of your site. The structure looks like this:

```markdown
# Project Name

> A one-line summary of what this project does.

A slightly longer description with context.

## Section Name

- [Link Title](https://example.com/path): Description of the resource
- [Another Link](https://example.com/other): What this resource provides
```

The key pieces:

- **H1 with the project name** (required)
- **Blockquote** with a brief summary
- **Description paragraph** with more context
- **H2 sections** with markdown link lists pointing to your endpoints and docs

Vercel uses this pattern for their own docs. You can see the index at [vercel.com/docs/llms.txt](https://vercel.com/docs/llms.txt). We'll build both an `llms.txt` index and an `llms-full.txt` that bundles everything into a single response.

\*\*Note: Why plain text?\*\*

The llms.txt file is served as `text/plain`, not `text/markdown`. This is intentional. Agents fetch it as raw text and parse the markdown structure themselves. The `text/plain` content type keeps it simple and universally accessible.

## Build the llms.txt endpoint

The starter already has `app/llms.txt/route.ts` with a TODO stub. Open it up and replace the placeholder content with the real llms.txt markup:

```ts title="app/llms.txt/route.ts"
import { NextResponse } from "next/server";

const llmsTxt = `# Cooking Course Feedback API

> API for submitting and retrieving student feedback on cooking course lessons.

This API serves feedback data for a cooking course platform. Students can submit ratings and comments on individual lessons, retrieve feedback filtered by course or rating, and view aggregate statistics.

## API Documentation

- [API Docs](/api/docs): Full endpoint reference with parameters, examples, and error cases
- [API Docs (Markdown)](/api/docs.md): Same documentation in .md format
- [Full Documentation](/llms-full.txt): Complete API docs in a single file

## Endpoints

- [List feedback](/api/feedback): GET all feedback entries, with optional filtering
- [Get feedback by ID](/api/feedback/:id): GET a single feedback entry
- [Submit feedback](/api/feedback): POST a new feedback entry
- [Feedback summary](/api/feedback/summary): GET aggregate statistics
`;

export async function GET() {
  return new NextResponse(llmsTxt, {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
    },
  });
}
```

The route folder name is literally `llms.txt`, which means Next.js will serve it at `/llms.txt`. The content follows the spec: H1 with the project name, a blockquote summary, a description, and H2 sections with links to everything an agent might need.

\*\*Note: The /api/docs link doesn't work yet\*\*

The llms.txt file links to `/api/docs`, but that route is still a stub with placeholder content. That's intentional. In Section 3, the skill you build will generate the real `/api/docs` route handler automatically. For now, the link establishes the contract: this is where agents will find the full documentation once it exists. The `.md` variant you're about to fill in serves as the working docs endpoint until then.

## Add llms-full.txt

The llms.txt file is an index. It tells agents what exists and where to look. But sometimes an agent doesn't want to follow links. It wants everything in one shot.

That's what `llms-full.txt` is for. Where `llms.txt` is the table of contents, `llms-full.txt` is the whole book. One request, one response, every endpoint documented. Vercel's docs site serves both: `llms.txt` for agents that want to browse selectively, and `llms-full.txt` for agents that want to load everything into context at once.

For a small API like ours, the full version isn't dramatically longer than the index. But the pattern matters. As your API grows, agents that only need one endpoint can start with `llms.txt` and follow a single link. Agents that need the full picture fetch `llms-full.txt` and get it all.

The starter has `app/llms-full.txt/route.ts` with a TODO stub. Fill it in with the project overview and the complete API documentation:

```ts title="app/llms-full.txt/route.ts"
import { NextResponse } from "next/server";

const llmsFullTxt = `# Cooking Course Feedback API

> API for submitting and retrieving student feedback on cooking course lessons.

This API serves feedback data for a cooking course platform. Students can submit ratings and comments on individual lessons, retrieve feedback filtered by course or rating, and view aggregate statistics.

## Endpoints

### List feedback

\`\`\`
GET /api/feedback
\`\`\`

Returns all feedback entries. Supports optional query parameters for filtering.

**Query parameters:**

| Parameter    | Type   | Description                          |
|--------------|--------|--------------------------------------|
| courseSlug   | string | Filter by course slug                |
| lessonSlug   | string | Filter by lesson slug                |
| minRating    | number | Only return entries rated >= value   |

**Example request:**

\`\`\`bash
curl "http://localhost:3000/api/feedback?courseSlug=knife-skills"
\`\`\`

**Example response:**

\`\`\`json
[
  {
    "id": "fb-001",
    "courseSlug": "knife-skills",
    "lessonSlug": "the-claw-grip",
    "rating": 5,
    "comment": "Finally understand why my onion cuts were uneven. The claw grip changed everything.",
    "author": "Priya Sharma",
    "createdAt": "2026-03-01T10:30:00Z"
  }
]
\`\`\`

### Get feedback by ID

\`\`\`
GET /api/feedback/:id
\`\`\`

Returns a single feedback entry.

**Example request:**

\`\`\`bash
curl "http://localhost:3000/api/feedback/fb-001"
\`\`\`

**Example response:**

\`\`\`json
{
  "id": "fb-001",
  "courseSlug": "knife-skills",
  "lessonSlug": "the-claw-grip",
  "rating": 5,
  "comment": "Finally understand why my onion cuts were uneven. The claw grip changed everything.",
  "author": "Priya Sharma",
  "createdAt": "2026-03-01T10:30:00Z"
}
\`\`\`

**Error response (404):**

\`\`\`json
{
  "error": "Feedback with id \\"fb-999\\" not found"
}
\`\`\`

### Submit feedback

\`\`\`
POST /api/feedback
\`\`\`

Creates a new feedback entry. The \`id\` and \`createdAt\` fields are generated automatically.

**Request body (JSON):**

| Field       | Type   | Required | Description                     |
|-------------|--------|----------|---------------------------------|
| courseSlug  | string | yes      | Slug of the course              |
| lessonSlug  | string | yes      | Slug of the lesson              |
| rating      | number | yes      | Rating from 1 to 5              |
| comment     | string | yes      | Feedback text                   |
| author      | string | yes      | Name of the person              |

**Example request:**

\`\`\`bash
curl -X POST "http://localhost:3000/api/feedback" \\
  -H "Content-Type: application/json" \\
  -d '{
    "courseSlug": "bread-baking",
    "lessonSlug": "scoring-dough",
    "rating": 5,
    "comment": "The lame technique demo was incredibly helpful.",
    "author": "Alex Turner"
  }'
\`\`\`

**Example response (201):**

\`\`\`json
{
  "id": "fb-011",
  "courseSlug": "bread-baking",
  "lessonSlug": "scoring-dough",
  "rating": 5,
  "comment": "The lame technique demo was incredibly helpful.",
  "author": "Alex Turner",
  "createdAt": "2026-03-06T12:00:00Z"
}
\`\`\`

**Error response (400), missing fields:**

\`\`\`json
{
  "error": "Missing required fields: courseSlug, lessonSlug, rating, comment, author"
}
\`\`\`

**Error response (400), invalid rating:**

\`\`\`json
{
  "error": "Rating must be a number between 1 and 5"
}
\`\`\`

### Get summary

\`\`\`
GET /api/feedback/summary
\`\`\`

Returns aggregate statistics across all feedback. Optionally filter by course.

**Query parameters:**

| Parameter   | Type   | Description            |
|-------------|--------|------------------------|
| courseSlug  | string | Filter by course slug  |

**Example request:**

\`\`\`bash
curl "http://localhost:3000/api/feedback/summary"
\`\`\`

**Example response:**

\`\`\`json
{
  "totalEntries": 10,
  "averageRating": 4.2,
  "ratingDistribution": {
    "1": 0,
    "2": 1,
    "3": 1,
    "4": 3,
    "5": 5
  },
  "courses": [
    {
      "courseSlug": "knife-skills",
      "totalEntries": 5,
      "averageRating": 4.4
    },
    {
      "courseSlug": "bread-baking",
      "totalEntries": 4,
      "averageRating": 4.0
    },
    {
      "courseSlug": "pasta-from-scratch",
      "totalEntries": 1,
      "averageRating": 4.0
    }
  ]
}
\`\`\`

## Schema

### Feedback

| Field       | Type   | Description                              |
|-------------|--------|------------------------------------------|
| id          | string | Unique identifier (e.g. "fb-001")        |
| courseSlug  | string | Slug of the course                       |
| lessonSlug  | string | Slug of the lesson                       |
| rating      | number | Integer from 1 to 5                      |
| comment     | string | Feedback text                            |
| author      | string | Name of the person                       |
| createdAt   | string | ISO 8601 timestamp                       |

## Workflows

### Investigate low-rated feedback for a course

1. \`GET /api/feedback/summary?courseSlug=knife-skills\` — check the average rating and total entries
2. \`GET /api/feedback?courseSlug=knife-skills&minRating=1\` — pull all low-rated entries
3. \`GET /api/feedback/fb-003\` — get the full details on a specific entry

### Submit and verify new feedback

1. \`POST /api/feedback\` — submit the feedback entry with all required fields
2. \`GET /api/feedback/:id\` — fetch the newly created entry using the \`id\` from the POST response
3. \`GET /api/feedback/summary?courseSlug=bread-baking\` — check updated stats for the course

### Compare feedback across courses

1. \`GET /api/feedback/summary\` — get aggregate stats for all courses
2. \`GET /api/feedback?courseSlug=knife-skills\` — pull all feedback for the top-rated course
3. \`GET /api/feedback?courseSlug=bread-baking\` — pull all feedback for comparison
`;

export async function GET() {
  return new NextResponse(llmsFullTxt, {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
    },
  });
}
```

This is a lot of content, but that's the point. The `llms-full.txt` file is the "give me everything" option. Notice it uses the same `text/plain` content type as `llms.txt`. Same spec, different scope.

\*\*Note: Index vs. full: two access patterns\*\*

`llms.txt` is for agents that want to browse. They read the index, pick the links they need, and fetch only what's relevant. `llms-full.txt` is for agents that want to load everything into context at once. Both patterns are useful. Small APIs can get away with just `llms-full.txt`, but offering both is the convention.

## Add markdown access to the docs

Vercel serves all their docs pages as `.md` too. If you can read `/docs/functions`, you can also read `/docs/functions.md`. This pattern makes it easy for agents to request documentation in a format they parse well.

We'll do the same thing. The starter already has `app/api/docs.md/route.ts` with a TODO stub. Open it and fill in the full API documentation.

Replace the placeholder in `app/api/docs.md/route.ts`:

```ts title="app/api/docs.md/route.ts"
import { NextResponse } from "next/server";

const docs = `# Feedback API

Base URL: \`/api/feedback\`

## Endpoints

### List feedback

\`\`\`
GET /api/feedback
\`\`\`

Returns all feedback entries. Supports optional query parameters for filtering.

**Query parameters:**

| Parameter    | Type   | Description                          |
|--------------|--------|--------------------------------------|
| courseSlug   | string | Filter by course slug                |
| lessonSlug   | string | Filter by lesson slug                |
| minRating    | number | Only return entries rated >= value   |

**Example request:**

\`\`\`bash
curl "http://localhost:3000/api/feedback?courseSlug=knife-skills"
\`\`\`

**Example response:**

\`\`\`json
[
  {
    "id": "fb-001",
    "courseSlug": "knife-skills",
    "lessonSlug": "the-claw-grip",
    "rating": 5,
    "comment": "Finally understand why my onion cuts were uneven. The claw grip changed everything.",
    "author": "Priya Sharma",
    "createdAt": "2026-03-01T10:30:00Z"
  }
]
\`\`\`

### Get feedback by ID

\`\`\`
GET /api/feedback/:id
\`\`\`

Returns a single feedback entry.

**Example request:**

\`\`\`bash
curl "http://localhost:3000/api/feedback/fb-001"
\`\`\`

**Example response:**

\`\`\`json
{
  "id": "fb-001",
  "courseSlug": "knife-skills",
  "lessonSlug": "the-claw-grip",
  "rating": 5,
  "comment": "Finally understand why my onion cuts were uneven. The claw grip changed everything.",
  "author": "Priya Sharma",
  "createdAt": "2026-03-01T10:30:00Z"
}
\`\`\`

**Error response (404):**

\`\`\`json
{
  "error": "Feedback with id \\"fb-999\\" not found"
}
\`\`\`

### Submit feedback

\`\`\`
POST /api/feedback
\`\`\`

Creates a new feedback entry. The \`id\` and \`createdAt\` fields are generated automatically.

**Request body (JSON):**

| Field       | Type   | Required | Description                     |
|-------------|--------|----------|---------------------------------|
| courseSlug  | string | yes      | Slug of the course              |
| lessonSlug  | string | yes      | Slug of the lesson              |
| rating      | number | yes      | Rating from 1 to 5              |
| comment     | string | yes      | Feedback text                   |
| author      | string | yes      | Name of the person              |

**Example request:**

\`\`\`bash
curl -X POST "http://localhost:3000/api/feedback" \\
  -H "Content-Type: application/json" \\
  -d '{
    "courseSlug": "bread-baking",
    "lessonSlug": "scoring-dough",
    "rating": 5,
    "comment": "The lame technique demo was incredibly helpful.",
    "author": "Alex Turner"
  }'
\`\`\`

**Example response (201):**

\`\`\`json
{
  "id": "fb-011",
  "courseSlug": "bread-baking",
  "lessonSlug": "scoring-dough",
  "rating": 5,
  "comment": "The lame technique demo was incredibly helpful.",
  "author": "Alex Turner",
  "createdAt": "2026-03-06T12:00:00Z"
}
\`\`\`

**Error response (400), missing fields:**

\`\`\`json
{
  "error": "Missing required fields: courseSlug, lessonSlug, rating, comment, author"
}
\`\`\`

**Error response (400), invalid rating:**

\`\`\`json
{
  "error": "Rating must be a number between 1 and 5"
}
\`\`\`

### Get summary

\`\`\`
GET /api/feedback/summary
\`\`\`

Returns aggregate statistics across all feedback. Optionally filter by course.

**Query parameters:**

| Parameter   | Type   | Description            |
|-------------|--------|------------------------|
| courseSlug  | string | Filter by course slug  |

**Example request:**

\`\`\`bash
curl "http://localhost:3000/api/feedback/summary"
\`\`\`

**Example response:**

\`\`\`json
{
  "totalEntries": 10,
  "averageRating": 4.2,
  "ratingDistribution": {
    "1": 0,
    "2": 1,
    "3": 1,
    "4": 3,
    "5": 5
  },
  "courses": [
    {
      "courseSlug": "knife-skills",
      "totalEntries": 5,
      "averageRating": 4.4
    },
    {
      "courseSlug": "bread-baking",
      "totalEntries": 4,
      "averageRating": 4.0
    },
    {
      "courseSlug": "pasta-from-scratch",
      "totalEntries": 1,
      "averageRating": 4.0
    }
  ]
}
\`\`\`

## Schema

### Feedback

| Field       | Type   | Description                              |
|-------------|--------|------------------------------------------|
| id          | string | Unique identifier (e.g. "fb-001")        |
| courseSlug  | string | Slug of the course                       |
| lessonSlug  | string | Slug of the lesson                       |
| rating      | number | Integer from 1 to 5                      |
| comment     | string | Feedback text                            |
| author      | string | Name of the person                       |
| createdAt   | string | ISO 8601 timestamp                       |

## Workflows

### Investigate low-rated feedback for a course

1. \`GET /api/feedback/summary?courseSlug=knife-skills\` — check the average rating and total entries
2. \`GET /api/feedback?courseSlug=knife-skills&minRating=1\` — pull all low-rated entries
3. \`GET /api/feedback/fb-003\` — get the full details on a specific entry

### Submit and verify new feedback

1. \`POST /api/feedback\` — submit the feedback entry with all required fields
2. \`GET /api/feedback/:id\` — fetch the newly created entry using the \`id\` from the POST response
3. \`GET /api/feedback/summary?courseSlug=bread-baking\` — check updated stats for the course

### Compare feedback across courses

1. \`GET /api/feedback/summary\` — get aggregate stats for all courses
2. \`GET /api/feedback?courseSlug=knife-skills\` — pull all feedback for the top-rated course
3. \`GET /api/feedback?courseSlug=bread-baking\` — pull all feedback for comparison
`;

export async function GET() {
  return new NextResponse(docs, {
    headers: {
      "Content-Type": "text/markdown; charset=utf-8",
    },
  });
}
```

The `.md` extension in the URL makes it explicit that this is markdown, which is a signal agents recognize. The `/api/docs` route still has placeholder content from the starter. In Section 3, the skill you build will generate the real implementation of `/api/docs` automatically. For now, `/api/docs.md` is the working docs endpoint.

\*\*Note: Why write docs by hand if the skill will generate them?\*\*

Good question. You need to know what correct output looks like before you can evaluate what a skill produces. The docs you're writing here become your reference point in Section 3. Once the skill is working, you won't maintain these strings by hand again.

## Try It

Start your dev server and test both new endpoints.

Fetch the llms.txt file:

```bash
curl http://localhost:3000/llms.txt
```

You should see the plain-text markdown with the project name, summary, and links to your API docs and endpoints (including the new `llms-full.txt` link).

Now fetch the full documentation:

```bash
curl http://localhost:3000/llms-full.txt
```

This should return the complete API documentation in a single response. It's the same content as `/api/docs.md` but with the project overview prepended.

Now fetch the markdown docs:

```bash
curl http://localhost:3000/api/docs.md
```

This should return the full API documentation in markdown.

Verify the content types are correct:

```bash
curl -I http://localhost:3000/llms.txt
```

Look for `Content-Type: text/plain; charset=utf-8` in the headers.

```bash
curl -I http://localhost:3000/api/docs.md
```

Look for `Content-Type: text/markdown; charset=utf-8` in the headers.

**Troubleshooting:**

- If you get a 404, make sure the folder names are exactly `llms.txt`, `llms-full.txt`, and `docs.md` inside `app/` and `app/api/` respectively. The folder name becomes the URL path.
- If the content type is wrong, double-check the `Content-Type` header string in your `NextResponse`. A typo like `text/plains` will silently serve the wrong type.

## Commit

```bash
git add -A
git commit -m "feat(docs): add llms.txt, llms-full.txt, and markdown docs access"
```

## Done-When

- [ ] Hitting `/llms.txt` returns a plain-text markdown file with H1, blockquote, description, and H2 sections linking to your endpoints
- [ ] Hitting `/llms-full.txt` returns the complete API documentation in a single plain-text response
- [ ] Hitting `/api/docs.md` returns the full API documentation in markdown
- [ ] The `/llms.txt` and `/llms-full.txt` responses have `Content-Type: text/plain; charset=utf-8`
- [ ] The `/api/docs.md` response has `Content-Type: text/markdown; charset=utf-8`
- [ ] The llms.txt content includes links to `/api/docs`, `/api/docs.md`, and `/llms-full.txt`

## Solution

The complete implementations are shown in the exercise above. Here are the three files:

- **`app/llms.txt/route.ts`** — Returns the project index as `text/plain` with H1, blockquote summary, and links to docs and endpoints
- **`app/llms-full.txt/route.ts`** — Returns the complete API documentation (all endpoints, schema, workflows) as `text/plain`
- **`app/api/docs.md/route.ts`** — Returns the same endpoint documentation as `text/markdown`, without the project overview header


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
