Integrating Mollie in Netlify Functions for serverless payments

I have been a long time Netlify user and fan, mostly using it for static websites (like this blog). Recently however, I had a requirement to accept donations on a site I run and started looking for ways to do this.

In this article I will describe the steps taken to implement Mollie payments on Netlify Functions for a fully serverless solution.

The full example source code can be found here: GitHub repository

Netlify Functions

A classical approach to this problem would be to build a backend to process payments and connect my Netlify frontend to it. But wouldn´t it be easier (and cheaper) if we can do this in a serverless fashion? Turns out we quite easily can!

Netlify has a concept called Functions, which is a serverless execution environment like any other. The free tier suffices for low to moderate usage.

You can write functions in Typescript, Javascript or Go and they run in a NodeJS (18 or later) environment.

This means that ideally, to save us work and time, we find a payment service provider with a NodeJS client library.

Mollie

There are always various options to choose from but in the end I settled with Mollie. Mollie is a well known European payment service provider and one of the main advantages for my use case was that there are no subscription fees, only transaction fees.

It also turns out there is an officially supported NodeJS API client library for Mollie, so let’s see if we can wire this up!

Setting things up

Mollie

You need to sign up as a business with Mollie in order to receive payments.

It takes a couple of steps and you need to supply some information, but the process is quite straightforward.

Once your account is created, you can find your API keys under https://my.mollie.com/dashboard/your_organization_id/developers/api-keys

Please make sure you use the Test API key until everything works as expected and you’re ready to receive payments.

Netlify

Getting started

If you already have a working Netlify site and are familiar with deploying there, you can skip to setting up Netlify Dev for local testing right away.

How I typically go about hosting stuff on Netlify is by linking sites to a GitHub repo, so they automatically get published whenever I merge something to main. This is a common use case on Netlify and is very easy to setup:

  • sign up with Netlify
  • create a new site and click “Import an existing project”
  • select “Deploy with GitHub”
  • you will need to authorize the Netlify application within GitHub (you can limit the repos Netlify has access to)
  • select the repo that contains your website
  • if your website is not in the root of the repo, configure a base directory in either netlify.toml or by going to Site Configuration -> Build & Deploy -> Build settings

Now, any time you commit something to main, your site will be published. This also goes for the function we’re about to add.

For more details and to fine tune configuration, see the documentation

Setting up Netlify Dev for local testing

While developing functions (or anything else for that matter), it’s extremely handy to be able to iteratively test your changes. Netlify CLI supports a way to run your Netlify site locally, so you can easily test without having to deploy anything.

First, install Netlify CLI:

1
$ npm install netlify-cli -g

You may or may not have to respawn your shell for path modifications before you can use the netlify command.

Now, login to Netlify from your command line:

1
$ netlify login

This will open a browser so you can authorize the CLI by logging in.

Once logged in, the final step is to link your site to your local project folder:

1
$ netlify link

Now, you can use commands like “netlify build” to build your project and also “netlify dev” to run a local development server for your website. The Netlify CLI also allows you to set environment variables.

For more information, see Netlify CLI

Setting up your API key in an environment variable

You need the API key from your Mollie account to communicate with the API and the safest way to do this is by storing it in an environment variable in Netlify.

There are two ways to do this:

Make sure you use the Test API key supplied by Mollie while you’re testing.

In the example, the variable is called MOLLIE_API_KEY but you can obviously deviate from this.

Creating a function

Routing

In Netlify, many things work by convention (although pretty much anything can be configured to work differently).

Function code is typically stored in a netlify/functions subfolder of your webroot and once deployed, will be accessible automagically on https://yoursite.com/.netlify/functions/helloworld . However, we can configure some redirects to make them accessible through a more commonly used URL pattern, like https://yoursite.com/api/helloworld. We do this by creating a netlify.toml file in the webroot:

1
2
3
4
5
6
7
8
[build]
  functions = "netlify/functions"  
  # base = "website" <- specify if you deviated from the default folder when creating the site

[[redirects]]
  from = '/api/*'
  to = '/.netlify/functions/:splat'
  status = 200

A netlify.toml file is picked up by Netlify during the build and deploy phases and allows you to configure your site in an expressive way.

In this case, we map everything from /api/ to /.netlify/functions/.

NPM

We need various NPM packages for things to work, so from the root of your website, run:

1
$ npm install @netlify/functions @mollie/api-client validator

The validator package is optional, but I use it for some input sanitization as you’ll see later.

.gitignore

In case you don’t have one yet, now would be a good time to add a .gitignore to exclude Netlify and NPM stuff from your repository:

.netlify

node_modules

Mollie.mts

The full source code can be found here and I’ll break it down here piece by piece.

Setting up a Mollie client

This first section retrieves your Mollie API key from the environment and instantiates a Mollie client with it:

1
2
3
4
5
6

const mollieApiKey = Netlify.env.get("MOLLIE_API_KEY");
const mollieClient = createMollieClient({ apiKey: mollieApiKey });

const maxAmount = 50000;

There is a payment amount limit per payment method in Mollie and you will get an error if you exceed this, so it is enforced on the backend. See this list for more information.

Entrypoint

This is the entrypoint for the Netlify function. It will get the original request and a Netlify context passed along.

It does some input validation and if the payment was succesfully created, the user is redirected to the Mollie checkout page (which is part of the created payment):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20

export default async (req: Request, context: Context) => {
    return req.formData().then(async (data : FormData) => {
      if (!data) {
        return new Response("Invalid form data", { status: 400 });
      }

      const amount = getAmountFromRequest(data);
      if (!amount) {
        return new Response("Invalid amount in request", { status: 400 });
      }

      const checkoutUrl = await createPayment(amount, getDescriptionFromRequest(data));
      if (!checkoutUrl) {
        return new Response("Failed to properly create payment", { status: 500 });      
      }

      return Response.redirect(checkoutUrl);
    });
}
Creating a payment

Creating an actual payment is a matter of using the client library with the right request:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

async function createPayment(amount: number, description: string) : string {
  amount = Math.min(amount, maxAmount);

  const payment = await mollieClient.payments.create({
    amount: {
      value: amount.toFixed(2),
      currency: 'EUR'
    },
    description: description || 'Payment from website',
    redirectUrl: 'https://www.yoursite.com/payment-completed.html',
    cancelUrl: 'https://www.yoursite.com/payment-cancelled.html'
  });

  return payment?._links?.checkout?.href;
}

Some things worth mentioning:

  • The amount limit is enforced on the backend, because it will result in an error
  • The amount itself is broken down in currency and value; value should be a string with two decimals
  • The redirectUrl is used to redirect clients after a payment was completed
  • The cancelUrl is used to redirect clients after a payment was cancelled
Input sanitization

Just as good practice, input sanitization takes place in two helper functions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function getAmountFromRequest(data: FormData) : number | null {
  const amount = data.get("amount");
  const amountAsNumber = Number(amount);

  return isNaN(amountAsNumber) ? null : amountAsNumber;
}

function getDescriptionFromRequest(data: FormData) : string | null {
  const description = data.get("description");

  if (!description || typeof description !== "string") {
    return null;
  }

  return validator.escape(description);
}

Testing and deploying

Testing can be done using Netlify Dev as described above and once satisfied, deployed to Netlify automatically when merged to main.

When you go live for real, don´t forget to switch to your Live API key!

Final words

That pretty much sums up the work required to start accepting Mollie payments in a serverless environment. It can be ideal for sites to start without any monetary investment and running costs.

Enjoy!