<!-- Generated from llms-txt-express/index.html. The canonical document is the HTML page. -->

- [ Home ](/) 
/
- [ How to create ](/how-to-create/) 
/
- Express.js guide            
# llms.txt for Express.js

Express is the most popular Node.js web framework. Serving llms.txt is trivial, either via express.static() for a static file or a dedicated GET route for dynamic generation.

Last updated: April 22, 2026

ℹ Which approach to use?

Use the  static file  approach if your  llms.txt  rarely changes and you
already serve static assets with  express.static() . Use a  GET route  if
you want to generate content dynamically from your database, CMS, or route metadata.

## Option 1: static file with express.static()

If your app already uses `express.static()` to serve a `public/` 
folder, simply drop `llms.txt` into that folder. Express will serve it at `/llms.txt` with no additional code.
server.js, static file approach   
Copy

```
// server.js
const express = require('express');
const app = express();

// Serve everything in ./public/ at the root URL.
// If public/llms.txt exists, it is available at /llms.txt automatically.
app.use(express.static('public'));

app.listen(3000, () => console.log('Listening on http://localhost:3000'));

// Project structure:
// your-express-app/
// ├── public/
// │   └── llms.txt   ← add this
// └── server.js

```

Express automatically sets the `Content-Type` to `text/plain` 
for `.txt` files served via `express.static()`. To add a `Cache-Control` header, pass `maxAge` in the static options:
Cache-Control with express.static()   
Copy

```
app.use(express.static('public', { maxAge: '1h' }));
```

## Option 2: dedicated GET route

For full control over headers and content, add a dedicated route. This is also the right
approach when the content is generated at runtime from a database or your API route registry.
server.js, GET route   
Copy

```
// server.js, dedicated GET route
const express = require('express');
const app = express();

const llmsContent = `# Your Site

> One-sentence description of what your site or product does.

## Documentation

- [Getting Started](https://yoursite.com/docs/start): Install and configure in minutes.
- [API Reference](https://yoursite.com/docs/api): Full endpoint catalog with examples.

## Product

- [Overview](https://yoursite.com/product): Core features and capabilities.
- [Pricing](https://yoursite.com/pricing): Plans and billing details.

## Optional

- [Changelog](https://yoursite.com/changelog): Release history.
- [GitHub](https://github.com/your-org/your-repo): Source code.
`;

app.get('/llms.txt', (req, res) => {
res.type('text/plain');
res.set('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400');
res.send(llmsContent);
});

app.listen(3000);

```

Place this route **before** any catch-all route handlers or 404 middleware, otherwise
Express will never reach it.

## TypeScript version

If your project uses TypeScript with `@types/express`, type the handler parameters
explicitly to avoid implicit `any` errors:
server.ts, TypeScript   
Copy

```
// server.ts, TypeScript version with typed Request/Response
import express, { Request, Response } from 'express';

const app = express();

const llmsContent = `# Your Site

> One-sentence description of your product or service.

## Documentation

- [Getting Started](https://yoursite.com/docs/start): Quick setup guide.
- [API Reference](https://yoursite.com/docs/api): Full endpoint catalog.

## Optional

- [Changelog](https://yoursite.com/changelog): Release history.
`;

app.get('/llms.txt', (req: Request, res: Response): void => {
res.set({
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
});
res.send(llmsContent);
});

app.listen(3000, () => console.log('Server running on http://localhost:3000'));

```

## Dynamic generation from routes metadata

For larger applications, maintain a `publicRoutes` array alongside your route definitions.
The `/llms.txt` handler maps this array to Markdown links, ensuring the file stays in sync
with your actual routes.
server.js, dynamic generation   
Copy

```
// server.js, generate llms.txt dynamically from route metadata
const express = require('express');
const app = express();

// Define your public routes with metadata
const publicRoutes = [
{ title: 'Getting Started', path: '/docs/start', description: 'Install and configure in minutes.' },
{ title: 'API Reference', path: '/docs/api', description: 'Full endpoint catalog with examples.' },
{ title: 'Authentication', path: '/docs/auth', description: 'OAuth 2.0 and API key setup.' },
{ title: 'Webhooks', path: '/docs/webhooks', description: 'Event payloads and retry policy.' },
{ title: 'Pricing', path: '/pricing', description: 'Plans and billing details.' },
];

const SITE_URL = process.env.SITE_URL || 'https://yoursite.com';

app.get('/llms.txt', (req, res) => {
const links = publicRoutes
.map((r) => `- [${r.title}](${SITE_URL}${r.path}): ${r.description}`)
.join('\n');

const body = [
'# Your Site',
'',
'> One-sentence description of your product.',
'',
'## Documentation',
'',
links,
].join('\n');

res.set({
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
});
res.send(body);
});

app.listen(3000);

```

Set `SITE_URL` as an environment variable so the same code works across local, staging,
and production environments.

## Cache-Control header

Always add a `Cache-Control` header. A one-hour TTL with `stale-while-revalidate` is a sensible default, it allows reverse proxies and CDNs (nginx,
Cloudflare, AWS CloudFront) to cache the response and serve stale content while revalidating in the
background:
Recommended Cache-Control   
Copy

```
res.set('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400');
```

If you use a CDN in front of Express, check that the CDN respects `Cache-Control` from the origin. Cloudflare honors it by default; AWS CloudFront requires
a cache policy that allows origin headers to pass through.

## Verify

After starting your server, confirm the file is served correctly:
Local verification   
Copy

```
# Check headers
curl -I http://localhost:3000/llms.txt
# Expected:
# HTTP/1.1 200 OK
# Content-Type: text/plain; charset=utf-8
# Cache-Control: public, max-age=3600

# Check content
curl http://localhost:3000/llms.txt | head -5
# Should print:  # Your Site
```

After deploying, run the same check against your live URL, then paste it into the [llms.txt validator](/validator/) for full spec compliance.

## Checklist before shipping

- File served at `/llms.txt` with `200 OK`. 
- `Content-Type: text/plain; charset=utf-8` is set. 
- `Cache-Control` header is present. 
- Exactly one H1 heading at the top of the file. 
- Blockquote summary immediately after the H1. 
- All URLs are absolute (`https://`). 
- Route is registered before any catch-all or 404 handlers. 
- Validator returns no errors: [llmtxt.info/validator/](/validator/)   
## Related guides

- [How to create llms.txt](/how-to-create/), templates and checklist. 
- [llms.txt format reference](/llms-txt-format/), spec details. 
- [Best practices](/best-practices/), what to include and what to skip. 
- [Validator](/validator/) · [Generator](/generator/).        
## Sources

- [ llmstxt.org, official spec ](https://llmstxt.org/)
- [ Express.js docs, express.static() ](https://expressjs.com/en/starter/static-files.html)
- [ Express.js docs, routing ](https://expressjs.com/en/guide/routing.html)           
On this page

- [ Option 1: static file with express.static() ](#static)
- [ Option 2: dedicated GET route ](#route)
- [ TypeScript version ](#typescript)
- [ Dynamic generation from routes metadata ](#dynamic)
- [ Cache-Control header ](#cache)
- [ Verify ](#verify)
- [ Checklist before shipping ](#checklist)
