Add llms.txt and Markdown Access
A menu posted outside a restaurant tells people what is available before they enter.
Your API needs a discoverable index that serves the same purpose.
An agent cannot discover documentation at /api/docs unless it knows the route exists. Without a conventional index, each prompt or integration must supply the URL.
The llms.txt standard provides a file at a well-known path that describes the API and links to its documentation.
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
- Fill in the llms.txt content in
app/llms.txt/route.ts(stub provided in starter) - Fill in the complete docs in
app/llms-full.txt/route.ts(stub provided in starter) - Fill in the markdown docs in
app/api/docs.md/route.ts(stub provided in starter) - Verify all three endpoints return the correct content types
The llms.txt standard
The llms.txt spec (from llmstxt.org) defines a simple markdown format that lives at the root of your site. The structure looks like this:
# 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 providesThe format includes:
- 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. We'll build both an llms.txt index and an llms-full.txt that bundles everything into a single response.
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:
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",
},
});
}Next.js serves the llms.txt route folder at /llms.txt. Its response includes the project name, a blockquote summary, a description, and sections linking to the API resources.
Add llms-full.txt
The llms.txt file is an index of available resources. Some clients instead need the complete documentation in one response.
llms-full.txt provides that complete response. Vercel's docs site offers llms.txt for selective browsing and llms-full.txt for loading the full documentation.
The size difference is small for this API. As an API grows, clients can use llms.txt to fetch one resource or llms-full.txt to load the complete reference.
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:
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",
},
});
}The longer response is expected because llms-full.txt includes every endpoint. It uses the same text/plain content type as llms.txt with a broader scope.
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:
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 identifies the response as Markdown. The starter's /api/docs route remains a placeholder until the skill replaces it in Section 3, so /api/docs.md is the working endpoint for now.
Try It
Start your dev server and test both new endpoints.
Fetch the llms.txt file:
curl http://localhost:3000/llms.txtYou 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:
curl http://localhost:3000/llms-full.txtThis 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:
curl http://localhost:3000/api/docs.mdThis should return the full API documentation in markdown.
Verify the content types are correct:
curl -I http://localhost:3000/llms.txtLook for Content-Type: text/plain; charset=utf-8 in the headers.
curl -I http://localhost:3000/api/docs.mdLook 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, anddocs.mdinsideapp/andapp/api/respectively. The folder name becomes the URL path. - If the content type is wrong, double-check the
Content-Typeheader string in yourNextResponse. A typo liketext/plainswill silently serve the wrong type.
Commit
git add -A
git commit -m "feat(docs): add llms.txt, llms-full.txt, and markdown docs access"Done-When
- Hitting
/llms.txtreturns a plain-text markdown file with H1, blockquote, description, and H2 sections linking to your endpoints - Hitting
/llms-full.txtreturns the complete API documentation in a single plain-text response - Hitting
/api/docs.mdreturns the full API documentation in markdown - The
/llms.txtand/llms-full.txtresponses haveContent-Type: text/plain; charset=utf-8 - The
/api/docs.mdresponse hasContent-Type: text/markdown; charset=utf-8 - The llms.txt content includes links to
/api/docs,/api/docs.md, and/llms-full.txt
Solution
The exercise above contains the complete implementations for these files:
app/llms.txt/route.ts: Returns the project index astext/plainwith links to the docs and endpointsapp/llms-full.txt/route.ts: Returns the complete API documentation astext/plainapp/api/docs.md/route.ts: Returns the endpoint documentation astext/markdownwithout the project overview
Was this helpful?