> ## Documentation Index
> Fetch the complete documentation index at: https://buttercms.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Express.js

> Build content-driven Express.js applications with ButterCMS. Covers setup, pages, collections, components, and blog integration.

## Overview

This integration guide shows you how to how to update your existing project to:

1. install the ButterCMS package
2. instantiate ButterCMS
3. create components to fetch and display each of the three ButterCMS content types: [Pages](../../core-concepts/content-types/pages), [Collections](../../core-concepts/content-types/collections), and [Blog Posts](../../core-concepts/content-types/blog-engine).

<Tip>
  In order for the snippets to work, you'll need to [setup your dashboard content schemas inside of ButterCMS](../buttercms-setup) first.
</Tip>

### Starter project

Or, you can jump directly to the starter project below, which will allow you to clone, install, run, and deploy a fully working
starter project that's integrated with content already inside of your ButterCMS account.

<Card title="Express.js Starter Project" icon="rocket" href="../starter-projects/expressjs">
  Hit the ground running with a pre-configured Express.js + ButterCMS setup.
</Card>

## Installation

<Tabs>
  <Tab title="npm">
    ```bash theme={null}
    npm install buttercms express ejs
    ```
  </Tab>

  <Tab title="yarn">
    ```bash theme={null}
    yarn add buttercms express ejs
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash theme={null}
    pnpm add buttercms express ejs
    ```
  </Tab>
</Tabs>

Add your API token to `.env`:

```bash theme={null}
BUTTERCMS_API_TOKEN=your_api_token
```

## Initialize the client

Create a reusable client instance:

<Tabs>
  <Tab title="JavaScript">
    ```javascript theme={null}
    // lib/buttercms.js
    const Butter = require('buttercms').default;

    const butter = Butter(process.env.BUTTERCMS_API_TOKEN);

    module.exports = butter;
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // lib/buttercms.ts
    import Butter from 'buttercms';

    const butter = Butter(process.env.BUTTERCMS_API_TOKEN!);

    export default butter;
    ```
  </Tab>
</Tabs>

<Info>
  For complete SDK documentation including all available methods and configuration options, see the [JavaScript SDK Reference](../sdks/javascript-sdk).
</Info>

## Basic setup

<Tabs>
  <Tab title="JavaScript">
    ```javascript theme={null}
    // app.js
    require('dotenv').config();
    const express = require('express');
    const butter = require('./lib/buttercms');

    const app = express();

    app.set('view engine', 'ejs');
    app.set('views', './views');

    app.listen(3000, () => {
      console.log('Server running on port 3000');
    });

    module.exports = app;
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // app.ts
    import 'dotenv/config';
    import express from 'express';
    import butter from './lib/buttercms';

    const app = express();

    app.set('view engine', 'ejs');
    app.set('views', './views');

    app.listen(3000, () => {
      console.log('Server running on port 3000');
    });

    export default app;
    ```
  </Tab>
</Tabs>

## Pages

<Tabs>
  <Tab title="JavaScript">
    ```javascript theme={null}
    // routes/pages.js
    const express = require('express');
    const router = express.Router();
    const butter = require('../lib/buttercms');

    router.get('/', (req, res) => renderPage('home', res));
    router.get('/:slug', (req, res) => renderPage(req.params.slug, res));

    async function renderPage(slug, res) {
      try {
        const response = await butter.page.retrieve('landing_page', slug);
        if (!response.data?.data) {
          throw new Error('Failed to load page from ButterCMS');
        }
        res.render('landing', { page: response.data.data });
      } catch (err) {
        res.status(404).render('404');
      }
    }

    module.exports = router;
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // routes/pages.ts
    import { Router, Request, Response } from 'express';
    import butter from '../lib/buttercms';

    const router = Router();

    interface PageFields {
      headline: string;
      subheadline: string;
      hero_image: string;
      body: string;
    }

    router.get('/', (req: Request, res: Response) => renderPage('home', res));
    router.get('/:slug', (req: Request<{ slug: string }>, res: Response) => renderPage(req.params.slug, res));

    async function renderPage(slug: string, res: Response) {
      try {
        const response = await butter.page.retrieve<PageFields>('landing_page', slug);
        if (!response.data?.data) {
          throw new Error('Failed to load page from ButterCMS');
        }
        res.render('landing', { page: response.data.data });
      } catch (err) {
        res.status(404).render('404');
      }
    }

    export default router;
    ```
  </Tab>
</Tabs>

```ejs theme={null}
<!-- views/landing.ejs -->
<!DOCTYPE html>
<html>
<head>
  <title><%= page.fields.headline %></title>
</head>
<body>
  <main>
    <h1><%= page.fields.headline %></h1>
    <p><%= page.fields.subheadline %></p>
    <% if (page.fields.hero_image) { %>
      <img src="<%= page.fields.hero_image %>" alt="<%= page.fields.headline %>" />
    <% } %>
    <%- page.fields.body %>
  </main>
</body>
</html>
```

## Collections

<Tabs>
  <Tab title="JavaScript">
    ```javascript theme={null}
    // routes/brands.js
    const express = require('express');
    const router = express.Router();
    const butter = require('../lib/buttercms');

    router.get('/', async (req, res) => {
      const response = await butter.content.retrieve(['brands']);
      if (!response.data?.data) {
        throw new Error('Failed to load brands from ButterCMS');
      }
      res.render('brands', { brands: response.data.data.brands });
    });

    module.exports = router;
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // routes/brands.ts
    import { Router, Request, Response } from 'express';
    import butter from '../lib/buttercms';

    const router = Router();

    interface Brand {
      name: string;
      logo: string;
      description: string;
    }

    router.get('/', async (req: Request, res: Response) => {
      const response = await butter.content.retrieve(['brands']);
      if (!response.data?.data) {
        throw new Error('Failed to load brands from ButterCMS');
      }
      const brands: Brand[] = response.data.data.brands;
      res.render('brands', { brands });
    });

    export default router;
    ```
  </Tab>
</Tabs>

```ejs theme={null}
<!-- views/brands.ejs -->
<main>
  <h1>Our Brands</h1>
  <ul>
    <% brands.forEach(brand => { %>
      <li>
        <img src="<%= brand.logo %>" alt="<%= brand.name %>" />
        <h2><%= brand.name %></h2>
        <%- brand.description %>
      </li>
    <% }); %>
  </ul>
</main>
```

## Dynamic components

### Component Renderer

<Tabs>
  <Tab title="JavaScript">
    ```javascript theme={null}
    // lib/component-renderer.js
    const componentTemplates = {
      hero: 'components/hero',
      features: 'components/features',
      testimonials: 'components/testimonials',
      cta: 'components/cta',
    };

    function getComponentTemplate(type) {
      return componentTemplates[type] || null;
    }

    module.exports = { getComponentTemplate };
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // lib/component-renderer.ts
    export interface ButterComponent {
      type: string;
      fields: Record<string, unknown>;
    }

    const componentTemplates: Record<string, string> = {
      hero: 'components/hero',
      features: 'components/features',
      testimonials: 'components/testimonials',
      cta: 'components/cta',
    };

    export function getComponentTemplate(type: string): string | null {
      return componentTemplates[type] || null;
    }
    ```
  </Tab>
</Tabs>

### Component template

```ejs theme={null}
<!-- views/components/hero.ejs -->
<section class="hero">
  <h1><%= fields.headline %></h1>
  <p><%= fields.subheadline %></p>
  <% if (fields.button_label) { %>
    <a href="<%= fields.button_url %>" class="btn"><%= fields.button_label %></a>
  <% } %>
  <% if (fields.image) { %>
    <img src="<%= fields.image %>" alt="<%= fields.headline %>" />
  <% } %>
</section>
```

### Using in routes

<Info>
  This uses a distinct page type (`component_page`) whose `body` is a **Component Picker** (Page Builder) field — an array of components — separate from the WYSIWYG `landing_page` in the Pages section (whose `body` is a string). A page's `body` is one field type or the other, so the Page Builder example needs its own page type.
</Info>

<Tabs>
  <Tab title="JavaScript">
    ```javascript theme={null}
    // routes/component-pages.js
    const express = require('express');
    const router = express.Router();
    const butter = require('../lib/buttercms');
    const { getComponentTemplate } = require('../lib/component-renderer');

    router.get('/:slug', async (req, res) => {
      try {
        const response = await butter.page.retrieve('component_page', req.params.slug);
        if (!response.data?.data) {
          throw new Error('Failed to load page from ButterCMS');
        }
        const page = response.data.data;

        // Map components to their templates
        const components = page.fields.body.map(component => ({
          template: getComponentTemplate(component.type),
          fields: component.fields,
        })).filter(c => c.template);

        res.render('component-page', { page, components });
      } catch (err) {
        res.status(404).render('404');
      }
    });

    module.exports = router;
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // routes/component-pages.ts
    import { Router, Request, Response } from 'express';
    import butter from '../lib/buttercms';
    import { getComponentTemplate, ButterComponent } from '../lib/component-renderer';

    const router = Router();

    interface ComponentPageFields {
      body: ButterComponent[];
    }

    router.get('/:slug', async (req: Request<{ slug: string }>, res: Response) => {
      try {
        const response = await butter.page.retrieve<ComponentPageFields>('component_page', req.params.slug);
        if (!response.data?.data) {
          throw new Error('Failed to load page from ButterCMS');
        }
        const page = response.data.data;

        const components = page.fields.body
          .map((component: ButterComponent) => ({
            template: getComponentTemplate(component.type),
            fields: component.fields,
          }))
          .filter((mapped): mapped is { template: string; fields: ButterComponent['fields'] } => mapped.template !== null);

        res.render('component-page', { page, components });
      } catch (err) {
        res.status(404).render('404');
      }
    });

    export default router;
    ```
  </Tab>
</Tabs>

```ejs theme={null}
<!-- views/component-page.ejs -->
<!DOCTYPE html>
<html>
<head>
  <title><%= page.fields.seo?.title || 'Page' %></title>
</head>
<body>
  <main>
    <% components.forEach(component => { %>
      <%- include(component.template, { fields: component.fields }) %>
    <% }); %>
  </main>
</body>
</html>
```

## Blog

<Tabs>
  <Tab title="Blog Post List">
    <Tabs>
      <Tab title="JavaScript">
        ```javascript theme={null}
        // routes/blog.js
        const express = require('express');
        const router = express.Router();
        const butter = require('../lib/buttercms');

        router.get('/', async (req, res) => {
          const page = Number(req.query.page) || 1;
          const response = await butter.post.list({ page, page_size: 10 });
          if (!response.data?.data) {
            throw new Error('Failed to load blog posts from ButterCMS');
          }
          res.render('blog/list', {
            posts: response.data.data,
            meta: response.data.meta,
          });
        });

        module.exports = router;
        ```
      </Tab>

      <Tab title="TypeScript">
        ```typescript theme={null}
        // routes/blog.ts
        import { Router, Request, Response } from 'express';
        import butter from '../lib/buttercms';

        const router = Router();

        router.get('/', async (req: Request, res: Response) => {
          const page = Number(req.query.page) || 1;
          const response = await butter.post.list({ page, page_size: 10 });
          if (!response.data?.data) {
            throw new Error('Failed to load blog posts from ButterCMS');
          }
          res.render('blog/list', {
            posts: response.data.data,
            meta: response.data.meta,
          });
        });

        export default router;
        ```
      </Tab>
    </Tabs>

    ```ejs theme={null}
    <!-- views/blog/list.ejs -->
    <main>
      <h1>Blog</h1>
      <ul>
        <% posts.forEach(post => { %>
          <li>
            <h2><a href="https://buttercms.com/blog/<%= post.slug %>"><%= post.title %></a></h2>
            <%- post.summary %>
            <span>By <%= post.author.first_name %> <%= post.author.last_name %></span>
          </li>
        <% }); %>
      </ul>
      <% if (meta.next_page) { %>
        <a href="/blog?page=<%= meta.next_page %>">Next Page</a>
      <% } %>
    </main>
    ```
  </Tab>

  <Tab title="Single Blog Post">
    <Tabs>
      <Tab title="JavaScript">
        ```javascript theme={null}
        // routes/blog.js (continued)
        router.get('/:slug', async (req, res) => {
          try {
            const response = await butter.post.retrieve(req.params.slug);
            if (!response.data?.data) {
              throw new Error('Failed to load blog post from ButterCMS');
            }
            res.render('blog/post', { post: response.data.data });
          } catch (err) {
            res.status(404).render('404');
          }
        });
        ```
      </Tab>

      <Tab title="TypeScript">
        ```typescript theme={null}
        // routes/blog.ts (continued)
        router.get('/:slug', async (req: Request<{ slug: string }>, res: Response) => {
          try {
            const response = await butter.post.retrieve(req.params.slug);
            if (!response.data?.data) {
              throw new Error('Failed to load blog post from ButterCMS');
            }
            res.render('blog/post', { post: response.data.data });
          } catch (err) {
            res.status(404).render('404');
          }
        });
        ```
      </Tab>
    </Tabs>

    ```ejs theme={null}
    <!-- views/blog/post.ejs -->
    <article>
      <h1><%= post.title %></h1>
      <p>
        By <%= post.author.first_name %> <%= post.author.last_name %>
        <% if (post.published) { %>
          on <%= new Date(post.published).toLocaleDateString() %>
        <% } %>
      </p>
      <% if (post.featured_image) { %>
        <img src="<%= post.featured_image %>" alt="<%= post.title %>" />
      <% } %>
      <%- post.body || '' %>
      <a href="/blog">Back to Posts</a>
    </article>
    ```
  </Tab>
</Tabs>

## Caching

Use a caching middleware for better performance:

```javascript theme={null}
// middleware/cache.js
const cache = new Map();
const CACHE_TTL = 3600000; // 1 hour

function butterCache(keyPrefix) {
  return async (req, res, next) => {
    const cacheKey = `${keyPrefix}_${req.originalUrl}`;
    const cached = cache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
      res.locals.butterCache = cached.data;
      return next();
    }

    // Store original render function
    const originalRender = res.render.bind(res);

    // Override render to cache the data
    res.render = (view, options) => {
      cache.set(cacheKey, {
        data: options,
        timestamp: Date.now(),
      });
      originalRender(view, options);
    };

    next();
  };
}

function invalidateCache(pattern) {
  for (const key of cache.keys()) {
    if (key.includes(pattern)) {
      cache.delete(key);
    }
  }
}

module.exports = { butterCache, invalidateCache };
```

### Webhook handler

```javascript theme={null}
// routes/webhooks.js
const express = require('express');
const router = express.Router();
const { invalidateCache } = require('../middleware/cache');

router.post('/buttercms', express.json(), (req, res) => {
  const { webhook_type } = req.body;

  if (webhook_type?.includes('published') ||
      webhook_type?.includes('updated') ||
      webhook_type?.includes('deleted')) {
    invalidateCache('butter');
  }

  res.json({ status: 'ok' });
});

module.exports = router;
```

## SEO

```javascript theme={null}
// middleware/seo.js
function extractSeoData(page) {
  const seo = page.fields.seo || {};
  return {
    title: seo.title || page.fields.headline || 'Page',
    description: seo.description || '',
    ogTitle: seo.og_title || seo.title || page.fields.headline,
    ogDescription: seo.og_description || seo.description,
    ogImage: seo.og_image || page.fields.hero_image || '',
  };
}

module.exports = { extractSeoData };
```

```ejs theme={null}
<!-- views/partials/head.ejs -->
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= seo.title %></title>
  <meta name="description" content="<%= seo.description %>">

  <!-- Open Graph -->
  <meta property="og:title" content="<%= seo.ogTitle %>">
  <meta property="og:description" content="<%= seo.ogDescription %>">
  <% if (seo.ogImage) { %>
    <meta property="og:image" content="<%= seo.ogImage %>">
  <% } %>
</head>
```

## Complete app setup

```javascript theme={null}
// app.js
require('dotenv').config();
const express = require('express');
const pagesRouter = require('./routes/pages');
const blogRouter = require('./routes/blog');
const brandsRouter = require('./routes/brands');
const webhooksRouter = require('./routes/webhooks');

const app = express();

app.set('view engine', 'ejs');

app.use('/blog', blogRouter);
app.use('/brands', brandsRouter);
app.use('/webhooks', webhooksRouter);
app.use('/', pagesRouter);

// 404 handler
app.use((req, res) => {
  res.status(404).render('404');
});

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).render('500');
});

app.listen(process.env.PORT || 3000);
```

## Resources

<CardGroup cols={2}>
  <Card title="Express.js Starter" icon="rocket" href="../starter-projects/expressjs">
    Pre-configured starter project
  </Card>

  <Card title="JavaScript SDK" icon="js" href="../sdks/javascript-sdk">
    Complete SDK reference
  </Card>

  <Card title="GitHub Repository" icon="github" href="https://github.com/ButterCMS/expressjs-starter-buttercms">
    View source code
  </Card>

  <Card title="Content API" icon="database" href="../../api-reference/pages/get-multiple-pages">
    REST API documentation
  </Card>
</CardGroup>
