Skip to main content

Netlify Functions

Postgres' branching capabilities were built with Netlify's Branch Deploys and Deploy Previews in mind. This makes Postgres a perfect fit for working with Netlify Functions.

In this guide, you'll learn how to deploy the blog project from the Migrations guide to a Netlify Function.

Prerequisites#

You must have a Netlify account connected to your GitHub account and a site configured to deploy the master branch of the repo you've been using for the Quick Start and Migrations guide. See Getting Started With GitHub for help setting up your project as a GitHub-hosted repository, and see the Netlify Docs for help setting up your repository as a Netlify site.

Lastly, install the encoding package. This is currently required by Netlify.

Setting up the Project#

  1. To make sure that platter clients are rebuilt on every push to GitHub, create a build command in your package.json's scripts that looks like this:

    {
    // the rest of package.json goes here
    "scripts": {
    "build": "BRANCH=$HEAD platter build"
    }
    }

    The build command takes a BRANCH environment variable (useful in the absence of git), and Netlify provides a HEAD environment variable during builds that maps to the name of the head branch in Deploy Preview build contexts. The BRANCH=$HEAD mapping means that Postgres clients are always built for the correct branch.

  2. Functions are JavaScript modules that export AWS lambda-style handler methods. They are automatically deployed from a configured directory. Start by creating a functions directory in your project root:

    mkdir ./functions

    ...and then adding the following to a netlify.toml in your project root:

    [build]
    functions = "functions"
    command = "npm run build"
    [[redirects]]
    from = "/*"
    to = "/.netlify/functions/:splat"
    status = 200
  3. For local development, you'll need an API key from the User Dashboard included in an .env file as PLATTER_API_KEY. Create this .env file with:

    echo 'PLATTER_API_KEY=<your-api-key>' > .env

    Do not commit .env files or Platter API keys to git! Instead, use a .gitignore file with the following contents:

    .env
    platter
  4. After all of this, your project should have the following structure:

    ├── .env
    ├── .gitignore
    ├── functions
    │   └── posts.js
    ├── migrations
    │ ├── $TIMESTAMP-add-authors.js
    │   └── $TIMESTAMP-add-posts.js
    ├── netlify.toml
    ├── platter
    │   └── postgres
    │   └── node
    │   ├── MyNewDatabase.d.ts
    │   └── MyNewDatabase.js
    └── package.json

Building Functions#

  1. In the functions directory, create a posts.js module with the following contents:

    const Postgres = require('../platter/postgres/node/MyNewDatabase')
    const postgres = new Postgres({ key: process.env.PLATTER_API_KEY })
    // create JSON responses from a payload and HTTP status code
    const createResponse = (payload, statusCode = 200) => ({
    statusCode,
    headers: {
    'Content-Type': 'application/json',
    },
    body: JSON.stringify(payload)
    })
    // export the API as a single handler
    exports.handler = async event => {
    // block non-read-only methods
    if (event.httpMethod !== 'GET') {
    return createResponse({ message: 'Method Not Allowed' }, 405)
    }
    // handle both /posts and /posts/:id paths
    const [id, ...rest] = event.path.split('/').filter(Boolean)
    // handle invalid paths of format /posts/:id/*
    if (rest.length) {
    return createResponse({ message: 'Not Found' }, 404)
    }
    try {
    if (id) {
    // handle /posts/:id
    const [post] = await postgres.query(
    'SELECT * FROM posts WHERE id = $1',
    [Number(id)]
    )
    if (post) {
    return createResponse(post)
    } else {
    return createResponse({ message: 'Not Found' }, 404)
    }
    } else {
    // handle /posts
    const posts = await postgres.query('SELECT * FROM posts')
    return createResponse(posts)
    }
    } catch (error) {
    const payload = { error, message: 'Internal Server Error' }
    return createResponse(payload, 503)
    }
    }
  2. For local development, use the Netlify CLI. The dev server for Functions can be invoked without installation with npx netlify dev. Once that is running (on port 8888 by default), you should be able to GET your posts with:

    curl http://localhost:8888/posts
    info

    While curl does the trick, we recommend httpie or ht for fetching data and jq for working with JSON responses, e.g. http :8888/posts | jq

    At this point, requests for posts should return an empty Array as a response.

  3. If everything is working as expected locally, then push your changes to master and go to your Netlify Dashboard to watch those functions deploy!

Branching Deploys#

At this point, you have a public API that mirrors the master branch of your project. With the build script in package.json configured above, you can now take advantage of Deploy Previews.

  1. Run npx platter watch and change branches to a branch called deploy-preview-test, like so:

    git checkout --branch deploy-preview-test

    This will create a deploy-preview-test branch of your my-new-database instance, rebuilding your Postgres client to point towards the deploy-preview-test database along the way.

  2. There are now two database branches (master and deploy-preview-test), and we need to seed the deploy-preview-test branch with some blog posts and authors. To fake those posts, install faker as a devDependency with:

    npm install --save-dev faker
  3. Create a seed file with the following command:

    mkdir ./seeds && touch ./seeds/add-posts.js

    Then add the following to your new add-posts.js file:

    const faker = require('faker')
    const Postgres = require('../platter/postgres/node/BlogApi')
    const postgres = new Postgres({ key: process.env.PLATTER_API_KEY })
    const seedBlogPosts = async () => {
    const transaction = await postgres.transaction()
    // create 5 authors with 3 posts each
    for (const _ of Array(5)) {
    const [{id}] = await transaction.query(
    `INSERT INTO authors (name )
    VALUES ($1)
    RETURNING id`,
    [faker.name.findName()]
    )
    for (const _ of Array(3)) {
    await transaction.query(
    `INSERT INTO posts (title, content, author_id)
    VALUES ($1, $2, $3)`,
    [faker.lorem.sentence(), faker.lorem.paragraphs(), id]
    )
    }
    }
    await transaction.commit()
    }
    seedBlogPosts().catch(console.error)
  4. Populate your database branch with:

    PLATTER_API_KEY=<your-api-key> node ./seeds/add-posts.js
  5. In addition to changing the data in the database, create an authors.js file in your functions directory to handle returning all authors, a single author by ID, or all of the posts associated with an author. That file should have the following content:

    const Postgres = require('../platter/postgres/node/MyNewDatabase')
    const postgres = new Postgres({ key: process.env.PLATTER_API_KEY })
    // create JSON responses from a payload and HTTP status code
    const createResponse = (payload, statusCode = 200) => ({
    statusCode,
    headers: {
    'Content-Type': 'application/json',
    },
    body: JSON.stringify(payload)
    })
    // export the API as a single handler
    exports.handler = async event => {
    // block non-read-only methods
    if (event.httpMethod !== 'GET') {
    return createResponse({ message: 'Method Not Allowed' }, 405)
    }
    // handle /authors, /authors/:id, and /authors/:id/posts paths
    const [id, resource, ...rest] = event.path.split('/').filter(Boolean)
    // handle invalid paths of format /authors/:id/(!posts)
    if (rest.length || (resource && resource !== 'posts')) {
    return createResponse({ message: 'Not Found' }, 404)
    }
    try {
    if (id) {
    // handle /authors/:id/posts
    if (resource) {
    const posts = await postgres.query(
    'SELECT * FROM posts WHERE author_id = $1',
    [Number(id)]
    )
    return createResponse(posts)
    }
    // handle /authors/:id
    const [author] = await postgres.query(
    'SELECT * FROM authors WHERE id = $1',
    [Number(id)]
    )
    if (author) {
    return createResponse(author)
    } else {
    return createResponse({ message: 'Not Found' }, 404)
    }
    } else {
    // handle /authors
    const authors = await postgres.query('SELECT * FROM authors')
    return createResponse(posts)
    }
    } catch (error) {
    const payload = { error, message: 'Internal Server Error' }
    return createResponse(payload, 503)
    }
    }
  6. If everything is working as expected locally, then push your changes to deploy-preview-test and go to your Netlify Dashboard to watch those functions deploy!

  7. Now your new deploy has both a new endpoint (/authors) and new data, while the "production" deploy of the master branch lacks both the new feature and the seed data.

Migrations#

To see how this branching would incoporate schema migrations, take a look at the Migrations Guide.