Beyond Page Routing: What Gatsby Functions Enable

Gatsby Functions bring server-side code to front-end developers without requiring them to provision or manage infrastructure. The feature follows Gatsby's established mental model: just as pages live in src/pages, serverless functions reside in src/api. That consistency means the developer experience for building an API endpoint feels no different from building a page.

The practical use cases are broad—newsletter signups via ConvertKit, sending email through SendGrid, persisting data to Fauna, or (the subject here) accepting Stripe payments. The common thread is that these third-party services require server-to-server communication, typically because they rely on secure keys. Executing those requests server-side keeps private credentials away from the browser and prevents abuse.

Same-Origin vs. Cross-Origin Functions

Most of the time, you'll use serverless functions the standard way: a website calls its own API. In this "same-origin" setup, both the front end and the API deploy to the same domain—www.my-website.com serves the UI and www.my-website.com/api handles the logic. Communication between the two is fast and frictionless.

Diagram of same origin function
A Gatsby website using its own Serverless Functions. (Large preview)

But there are at least two scenarios where a function may live on a different origin from the site that consumes it:

  1. The origin website cannot run serverless functions at all.
  2. The same serverless function is needed by more than one origin.
Diagram of cross origin function
Two websites using a Gatsby API’s Serverless Functions. (Large preview)

In the diagram above, website-1.com is a Gatsby site that could host its own functions but chooses not to. website-2.com is built on a platform with no serverless capabilities. Both need to reach the same third-party service, so the sensible approach is to abstract that functionality into a standalone API (my-api.com), which is itself a Gatsby site with its own function endpoints. Other origins can then call those endpoints—and, yes, that raises the CORS question, which we'll address shortly.

This pattern isn't new to Gatsby Functions. Before their release in June 2021, the same architecture could be built using Netlify Functions. One early experiment involved server-to-server communication between a Gatsby blog and the Twitter API v2, running through Netlify Functions. The same approach was later refactored to use Gatsby's own functions.

Monetizing MDX Embed

MDX Embed is an open-source project that lets you drop third-party media—YouTube videos, Tweets, Instagram posts, Spotify tracks, and more—directly into .mdx files without imports. Its documentation site is built with Storybook, which has no serverless capabilities. To add a payment layer, the project needed an API it could call from that origin.

The solution was Paulie API, a standalone Gatsby site that accepts requests from different origins and proxies them to services like Stripe. The api/make-stripe-payment endpoint handles checkouts: it receives the relevant payment details from MDX Embed, passes them through its own serverless function to the Stripe API, and Stripe returns a checkout URL. That URL is handed back to MDX Embed, which opens it in a new browser window where customers enter their payment details on Stripe's secure webpage.

Diagram of MDX Embed using Paulie API
MDX Embed connecting to the Stripe API via Paulie API’s Serverless Functions. (Large preview)

Why Not Just Use react-stripe-js?

react-stripe-js is Stripe's client-side toolkit for React. It lets you build checkouts entirely in the browser, without any custom server code. That works well when every customer pays the same price. But MDX Embed wanted a "Pay what you want" model, and a fixed price won't fund an open-source project.

Screenshot of Stripe dashboard with a price of $1.00 for the MDX Embed Product
Stripe dashboard product section. (Large preview)

Setting a dynamic price—one chosen by the customer—requires overriding the amount configured in the Stripe dashboard for a given product. That means a custom HTTP request with server-to-server communication, which is exactly where the Gatsby Function comes in. The function accepts a dynamic value and uses it to create the checkout, replacing the static dashboard price.

On the MDX Embed side, an HTML <input type="number" /> lets the visitor decide the contribution amount. The input value is passed to Paulie API, which forwards it to Stripe. This makes the checkout genuinely dynamic, letting contributors set what the project is worth to them.

How MDX Embed, Paulie API and the Stripe API work together to enable “Pay what you want”.

Credit for demonstrating this approach goes to Benedicte Raae, who presented it in her Summer Functions course at Queen Raae Codes.

Handling CORS for a Cross-Origin Endpoint

Gatsby Functions don't encounter CORS by default because the front end and API share an origin. For a cross-origin setup, however, the API must explicitly allow requests from other domains. The api/make-stripe-payment endpoint does this by defining an allowedOrigins array, and any request from an origin outside that list receives a status code 403 with the message 🚫 Request blocked by CORS.

// src/api/make-stripe-payment

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY)
import Cors from 'cors'

const allowedOrigins = [
  'https://www.mdx-embed.com',
  'https://paulie.dev',
]

const cors = Cors({
  origin: (origin, callback) => {
    if (allowedOrigins.includes(origin)) {
      callback(null, true)
    } else {
      callback(new Error())
    }
  },
})

const runCorsMiddleware = (req, res) => {
  return new Promise((resolve, reject) => {
    cors(req, res, (result) => {
      if (result instanceof Error) {
        return reject(result)
      }
      return resolve(result)
    })
  })
}

export default async function handler(req, res) {
  const { success_url, cancel_url, amount, product } = req.body

  try {
    await runCorsMiddleware(req, res)

    try {
      const session = await stripe.checkout.sessions.create({
        success_url: success_url,
        cancel_url: cancel_url,
        payment_method_types: ['card'],
        line_items: [
          {
            quantity: 1,
            price_data: {
              unit_amount: amount * 100,
              currency: 'usd',
              product: product,
            },
          },
        ],
        mode: 'payment',
      })

      res.status(200).json({ message: '🕺 Stripe checkout created ok', url: session.url })
    } catch (error) {
      res.status(500).json({ message: '🚫 Stripe checkout error' })
    }
  } catch (error) {
    res.status(403).json({ message: '🚫 Request blocked by CORS' })
  }
}

The function also accepts body parameters. One is the amount—the value entered in the HTML input on the MDX Embed site. Another is product, which maps to the product ID defined in the Stripe dashboard and tells Stripe which checkout to create. Passing product as a parameter rather than hardcoding it means the same endpoint can serve multiple Stripe products.

Why This Architecture Pays Off

Running an API as a Gatsby site might seem like extra work compared with a single origin hosting both the site and its functions. But there are compounding benefits.

Since Paulie API is both a cross-origin API and a regular Gatsby website, it doubles as documentation. And not just static docs—each function page includes a ▶ Run in browser link that takes visitors to an interactive playground. Visiting a page lets you exercise the function directly, which is useful both while developing and for demonstrating what the endpoint does.

Diagram of Paulie API’s cross origin functions
Paulie API cross origin function capabilities. (Large preview)

The reusable endpoint has already paid off: paulie.dev uses the same make-stripe-payment function for its own "Pay what you want" contribution feature. No duplication was needed—the endpoint just works from another site.

For features unique to one site, there's no need to abstract them yet. Paulie.dev also runs its own Gatsby serverless functions—handling post reactions stored in Fauna and capturing newsletter signups. Should another site ever need newsletter signup too, those functions would be migrated over to Paulie API, following the same pattern.

Screenshot of paulie.dev “Pay what you want” user interface
paulie.dev’s “Pay what you want” section. (Large preview)

When Abstraction Makes Sense

Moving your Serverless Functions behind an abstraction might feel like a step backward. One of the main attractions of serverless is keeping front-end and back-end code together in the same repository. However, as the guide demonstrates, there are valid scenarios where abstracting the payment logic away from the Gatsby site is the better move.

The author reports clear benefits from this approach and is extending their API to power several of their own websites. If you're interested in monetizing open-source software but your site isn't built on Gatsby, this pattern of separating the payment layer could still be the answer.

If you'd rather avoid the abstraction and keep everything inside Gatsby, the Gatsby Functions documentation is the starting point for wiring up your own endpoints.

Learning More About Serverless

For further reading on Serverless Functions, the article recommends a few community resources:

The FuncJam Challenge

If you're looking for inspiration or a push to finish an implementation, the Gatsby team was running a community competition called FuncJam. When time permits, you can join the challenge. The announcement post also contains a Byte-size section with helpful videos and links to several example functions to get you unstuck.

Further Reading

Smashing Editorial