As you (might) know, our current website is built on Gatsby. I love the fact we’re generating a super cacheable, fully static site with every build, but this adds a couple of limitations.

One of the limitations of this setup is having no backend (I know that sounds funny). At times, mostly when communicating with 3rd party services (newsletters, emails, captcha flows, etc.), we need a backend to handle authentication.

I felt that the natural way to approach this was using serverless functions (“’other people’s servers”’ wasn’t that catchy).

Serverless functions are built using these steps:

  1. create a function (which can access secrets and 3rd party APIs)
  2. deploy it to a platform
  3. call it as an API from JavaScript (just using something as simple as fetch)

I looked into several services (hardcore AWS, Cloudflare Workers, Serverless, and more) but using the Vercel platform (formerly known as Zeit) turned out to be the simplest solution.

What I like about Vercel’s approach is that it’s very easy to run things locally and you don’t have to do crazy stuff (looking at you AWS) to add external dependencies (aka NPM modules).

You can write these functions in Go, Python, Node, or Ruby. I’ll be focusing on Node since this is my go-to server language. Deployment is done through their CLI. I’ll also be using Next.js (I’ll dedicate a blog post to this framework later) to make the setup a bit easier.

This might look a bit complicated, but bear with me — it’s not that bad :).

The first step is to create an account on Vercel and authenticate with the CLI.

npm i -g vercel@latest
vercel login

Now also provides us with the option to start from a boilerplate by using now init.

vercel init nextjs

Make sure you rename the folder and the name property in package.json as your Next step (see what I did there).

Navigate into the folder, install the dependencies and run the project in development mode.

cd serverlessfunctions101
yarn
yarn dev

When navigating to http://localhost:3000, you should see a nice and clean introduction page. 

Now (I did it again), empty the /pages folder. We want to focus on functions only.

Add an /api folder inside of /pages and create hello-world.js there.

Your structure should look like this.

/pages
  /api
    /hello-world.js

Each file in /api will map to a route. Our function will be available on http://localhost:3000/api/hello-world

Creating a function is easy; each function receives a request and should return a response (if not, it will time out).

The simplest way to return data is by using response.send. This method accepts text, an object, or a buffer. 

It’s not a real tutorial if we’re not starting with a ‘hello world’ right?

export default (request, response) => 
  response.send('hello world');
export default (request, response) =>
  response.send({
    data: 'hello world',
  });

By default, the response status code is 200 (OK). You can modify the status code by using response.status. http-status-codes is a handy package to make this a bit more readable.

yarn add http-status-codes
import Status from 'http-status-codes';

// GET -> http://localhost:3000/api/hello-world

export default (request, response) =>
  response.status(Status.FORBIDDEN).json({ error: 'Unauthorized' });

We have access to the HTTP request method by using request.method.

import Status from 'http-status-codes';

// GET -> http://localhost:3000/api/hello-world

export default (request, response) => {
  if (request.method !== 'GET') {
    return response.status(Status.BAD_REQUEST).send('');
  }
  return response.json({
    data: 'hello world',
  });
};

request.query allows you to access query string data.

import Status from 'http-status-codes';

// GET -> http://localhost:3000/api/hello-world?name=Geoffrey

export default (request, response) => {
  if (request.method !== 'GET') {
    return response.status(Status.BAD_REQUEST).send('');
  }
  const name = request?.query?.name ?? 'world';
  return response.json({
    data: `hello ${name}`,
  });
};

request.body contains all data passed with the request

import Status from 'http-status-codes';

// POST -> `http://localhost:3000/api/hello-world`
// with JSON payload {"name": "Geoffrey"}

export default (request, response) => {
  if (request.method !== 'POST') {
    return response.status(Status.BAD_REQUEST).send('');
  }
  const name = request?.body?.name ?? 'world';
  return response.json({
    data: `hello ${name}`,
  });
};

async/await is supported by default, so you can easily call external APIs too.

export default async (request, response) => {
  const response = await fetch(apiURL);
  const data = await response.json();
  return response.send(data);
};

When it’s time to deploy your functions, it’s just a single command. Vercel guides you through the process and deploys to the correct target.

vercel deploys to a unique staging environment

vercel –prod deploys to production

Run the first command. Your function(s) should now be deployed to a *.vercel.app address, you can configure domains etc via your dashboard.

Sometimes you need to store secrets, you can configure those in the project’s settings. Locally, you can store your secrets in a .env.local file, like this.

API_KEY=my-secret-key

I would advise you to try out some things yourself. I promise you’ll really enjoy the experience of creating and deploying serverless functions with Vercel. This is just the tip of the iceberg when it comes to serverless, including CORS and middleware.

More information and documentation can be found on the Vercel website.