Overview

In this guide, you will set up and deploy a serverless Node.js application to Vercel (Formerly Zeit Now). The application will expose a REST API and use Prisma Client to handle fetching, creating, and deleting records from a database.

Vercel is a cloud platform for static sites and serverless functions. Typically Vercel integrates with a Git repository for automatic deployments upon commits. For the sake of simplicity, this guide uses the Vercel CLI which allows deploying directly from the command line.

The application has the following components:

  • Backend: Serverless Node.js REST API with resource endpoints that use Prisma Client to handle database operations against a PostgreSQL database (e.g. hosted on Heroku).
  • Frontend: Static HTML page to interact with the API.

architecture diagram

The focus of this guide is showing how Prisma integrates with Vercel. The starting point will the Prisma Vercel example which has a couple of REST endpoints preconfigured as serverless functions and a static page.

Throughout the guide you'll find various checkpoints that enable you to validate whether you performed the steps correctly.

Prerequisites

  • Hosted PostgreSQL database and a URL from which it can be accessed, e.g. postgresql://username:password@your_postgres_db.cloud.com/db_identifier (you can use Heroku, which offers a free plan).
  • Vercel account.
  • Vercel CLI installed.
  • Node.js installed.
  • PostgreSQL CLI psql installed.

Prisma workflow

Prisma supports different workflows depending on whether you integrate with an existing database or create a new one from scratch. Regardless of the workflow, Prisma relies on the Prisma schema, i.e. schema.prisma file.

This guide starts with an empty database created with plain SQL and looks as follows:

  1. Define the database schema using SQL.
  2. Run prisma introspect which will introspect and populate the prisma.schema with models based on the database schema
  3. Run prisma generate which will generate Prisma Client based on the Prisma schema.

1. Download the example and install dependencies

Open your terminal and navigate to a location of your choice. Create the directory that will hold the application code and download the example code:

$mkdir prisma-vercel
$cd prisma-vercel
$curl https://codeload.github.com/prisma/prisma-examples/tar.gz/latest | tar -xz --strip=3 prisma-examples-latest/deployment-platforms/vercel/

Checkpoint: ls -1 should show:

$ls -1
$README.md
$api
$vercel.json
$package.json
$prisma
$public
$schema.sql

Install the dependencies:

npm install

2. Vercel login

Make sure you're logged in to Vercel with the CLI:

$vercel login

This will allow you to deploy to Vercel from the terminal.

Checkpoint: vercel whoami should show your username:

$vercel whoami
$Vercel CLI 19.0.1
$> your-username

3. Create the database schema

To create your database schema, run the schema.sql from the example code as follows:

$psql postgresql://__USER__:__PASSWORD__@__HOST__/__DATABASE__ -f schema.sql

Note that the long string starting with postgresql:// will be referred to as the DATABASE_URL which you'll also need for the subsequent steps. You need to replace the uppercase placeholders with your database credentials, e.g.:

$psql postgresql://janedoe:randompassword@yourpostgres.compute-1.amazonaws.com:5432/yourdbname -f schema.sql

Checkpoint: psql $DATABASE_URL -c "\dt" should show the list of tables:

List of relations
Schema | Name | Type | Owner
-------+---------+-------+----------------
public | Post | table | janedoe
public | Profile | table | janedoe
public | User | table | janedoe

Congratulations, you have successfully created the database schema.

4. Set the DATABASE_URL environment variable

Set the DATABASE_URL environment variable locally so that Prisma can access the database to introspect:

$export DATABASE_URL="postgresql://__USER__:__PASSWORD__@__HOST__/__DATABASE__"

It's considered best practice to keep secrets out of your codebase. If you open up the prisma/schema.prisma file, you should see env("DATABASE_URL") in the datasource block. By setting an environment variable you keep secrets out of the codebase.

5. Introspect the database

Introspect the database with the Prisma CLI:

$npx prisma introspect

Prisma will introspect the database defined in the datasource block of the Prisma schema and populate the Prisma schema with models corresponding to the database tables.

Checkpoint: prisma/schema.prisma should look as follows (note that the fields on the models have been reordered for better readability):

model Post {
post_id Int @default(autoincrement()) @id
title String
content String?
author_id Int?
User User? @relation(fields: [author_id], references: [user_id])
}
model Profile {
profile_id Int @default(autoincrement()) @id
bio String?
user_id Int
User User @relation(fields: [user_id], references: [user_id])
}
model User {
user_id Int @default(autoincrement()) @id
name String?
email String @unique
Post Post[]
Profile Profile[]
}

Rename the relation fields for easy access

Because both the generated Post and Profile fields in the User model are virtual (i.e. they're not backed by a foreign key in the database), you can manually rename them in your Prisma schema. This will only affect the generated client and is typically done so that they have a more meaningful name in the context of the relation.

In the resulting Prisma schema there are two types of relation fields:

  • Relation fields: identified by having a Model name as the type, e.g. the User field in the Post model. Can be renamed to better fit its usage, e.g. User -> author.
  • Relation scalar fields: these are used to store the foreign key, e.g. the authorId field in the Post model. Cannot be rename as it must match the field in the database.

The names of the relation fields are used in the client to access those relations for example, fetching a specific Post and its associated User object would as follows with the Prisma schema above:

const postAuthor = await prisma.post.findOne({
where: { id: 1 },
include: { User: true },
})

If you rename the User field in the Post model to author, you'll be able to access it as follows:

const postAuthor = await prisma.post.findOne({
where: { id: 1 },
include: { author: true },
})

Based on that logic, rename the relation fields to better adhere to the naming conventions :

model Post {
post_id Int @default(autoincrement()) @id
author_id Int?
content String?
title String
author User? @relation(fields: [author_id], references: [user_id]) // renamed from `User` -> `author`
}
model Profile {
bio String?
profile_id Int @default(autoincrement()) @id
user_id Int
user User @relation(fields: [user_id], references: [user_id]) // renamed from `User` -> `user`
}
model User {
email String @unique
name String?
user_id Int @default(autoincrement()) @id
posts Post[] // renamed from `Post` -> `posts`
profiles Profile[] // renamed from `User` -> `profiles`
}

6. Define Vercel secret and expose to functions

In step 4 you set the DATABASE_URL environment variable on your machine. For the application to work, DATABASE_URL also needs to be available to Vercel's serverless functions.

Adding environment variables to Vercel requires two steps:

  1. Defining a Vercel secret via the Vercel CLI with vercel secrets add.
  2. Exposing the secret to your serverless functions in your vercel.json file.
$vercel secrets add database_url "$DATABASE_URL"

This uses the $DATABASE_URL environment variable defined in step 4.

Using the vercel.json configuration file, you can set Vercel related configuration, such as routing and environment variables. The vercel.json from the example will contain the configuration for making the secret available as an environment variable:

// vercel.json
{
// ...
"env": {
"DATABASE_URL": "@database_url"
},
"build": {
"env": {
"DATABASE_URL": "@database_url"
}
}
}

The configuration exposes the Vercel secret database_url as an environment variable named DATABASE_URL to both the build and run time. Prisma will use DATABASE_URL to connect to your database whenever the functions run.

7. Deploy to Vercel

Deploy the app from the project's root:

$vercel

After prompting you to pick a Vercel project name, it will deploy the app and log the preview URL.

Checkpoint: Make a request to the preview URL's API root:

$curl https://PROJECT_NAME.NOW_USERNAME.now.sh/api

The call should return: {"up":true}

8. Test your deployed application

You can use the static frontend to interact with the API you deployed via the preview URL.

Open up the preview URL in your browser, the URL should like this: https://PROJECT_NAME.NOW_USERNAME.now.sh. You should see the following:

deployed-screenshot

The four buttons allow you to make requests to the REST API and view the response:

  • Check API status: Will call the REST API status endpoint that returns {"up":true}. The implementation code is in api/index.js
  • Seed data: Will delete all database records and load the database with test data users, profiles, and posts. Returns the created users. The implementation code is in api/seed.js
  • Load users with profiles: Will load all users in the database with their related profiles. The implementation code is in api/getUsers.js
  • Load Posts: Will load posts and their related authors. The implementation code is in api/getPosts.js

For example, calling seed data should show the following:

deployed-seed-endpoint-screenshot

Notes

The package.json uses the postinstall hook script to run prisma generate. Typically this would go in the build step. Because Vercel caches node_modules after the dependencies are installed, the functions won't have access to the generated Prisma Client. Generating the Prisma Client in postinstall ensures that the generated Prisma Client in node_modules/@prisma/client is available to the functions.

Summary

Congratulations! You have successfully deployed the application to Vercel.

For more insight into Prisma Client's API, look at the function handlers in the api/ folder.

Generally, when using a FaaS (function as a service) environment to interact with a database, it's beneficial to pool DB connections for performance reasons. This is because every function invocation may result in a new connection to the database (this is not a problem with a constantly running node.js server). For more information on some of the solutions, you may want to look at our general FaaS guide.

Edit this page on Github