Back to blog overview

March 5, 2024

How to Store Next.js Form Data with Vercel Postgres

Sunjay Armstead



UI/UX Designer and Engineer II
Louisburg, NC

In a previous article, we dove deep into validating forms in Next.js with Zod. But, if you’re like me, you’re already thinking about the next logical step: just where, exactly, can we store that contact form data? The answer to that question largely depends on the scope, budget, and scalability concerns of your application. In this post, we’ll focus on smaller apps that take in just a handful of form responses each month.

## A Brief Look at the Vercel Ecosystem

Next.js is a framework maintained by robust community support and Vercel, a frontend cloud provider. As such, my experience deploying Next.js apps to Vercel has been nearly seamless.

In addition to standard deployment options, Vercel also offers serverless storage products, including Postgres instances. Lucky for us, Vercel’s (currently 😏) generous free tier allows us to store our lightweight contact form data for free!

So, given this context, this post makes the following assumptions:

1. You are developing a Next.js application (version 14.x)
2. Your app is currently deployed on Vercel

## A few Simple Steps

### Create a Database

In your Vercel dashboard, select the “Storage” tab and choose “Postgres” to create a new database instance. You’ll then be prompted to create a database name and select a region to store your data.

Screenshot of the Storage view in the Vercel dashboard

Behind the scenes, Vercel integrates with Neon which is its own managed Postgres service. This may become particularly important if you need to utilize specific Postgres extensions that may or may not be supported by Neon.

Screenshot of the database creation steps in the Vercel dashboard

### Configure Environment Variables

When your development pipeline exists within the Vercel ecosystem, you can pull down the environment variables you need to connect to your database. Here’s how that works:

1. Select “Connect Project” in your database settings and choose the Vercel deployment that corresponds to your project. The “Connect Project” button will prompt you to choose which environments should have access to the database. We’ll keep things simple and select all of the environments (Production, Preview, and Development).
2. On your development machine, follow Vercel's instructions to install the Vercel CLI using your preferred package manager (for example, npm i -g vercel).
3. On your development machine, navigate to your app’s root directory in the command line and run `vercel link`. The CLI will prompt you to login, ask you a few questions about your project on Vercel, and create a `.vercel` directory in your root directory.
4. Still within your app’s root directory, run `vercel env pull .env.development.local` to pull down the environment variables you’ll need from Vercel.

### Add your Code

After your database and environment variables are set up, install Vercel’s Postgres SDK using your preferred package manager. Our accompanying example repo uses NPM, so I’ll run npm i @vercel/postgres. You can now start adding SQL logic!

## A Quick Example

Your database structure is totally up to you and Vercel Postgres imposes few limitations on your creativity. But, since you’re here (thanks for hanging out), below is an example of how to save data to a messages table in your database. For simplicity, we won’t use an ORM like Prisma, although Vercel Postgres does support ORMs.

### Structure

Our SQL logic will ultimately run from within our server-action.ts file (see the GitHub repo for an updated view into this file). To keep things clean and reusable, we will import two additional actions: one for adding a table and one for adding table rows.

### Create the Table (if it Doesn’t Exist)

When your application runs, the `messages` table may or may not already exist. Let’s create a function that accounts for that:

// create-table-action.ts

import { sql } from "@vercel/postgres";

export const createTableIfNotExists = async () => {
 try {
   await sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`;
   await sql`CREATE TABLE IF NOT EXISTS "messages" (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(250), email VARCHAR(250), message VARCHAR(250), created_at timestamp DEFAULT now() NOT NULL);`;
   console.log("Successfully initialized the messages table")
 } catch (error) {
   throw new Error(`Yikes! We ran into an error: ${error}`)


What’s going on here? At the beginning of our try..catch statement, we fire off an SQL command to add a new extension (if it isn’t already registered in our database) that will enable our primary keys to be UUID values. And don’t worry, Neon has a full list of extensions available so you don’t have to guess (see the above discussion on how Vercel uses Neon behind the scenes). You can skip this step if you simply want integers for your primary keys.

(As a side note, my friends Jesse and Nick wrote an entire blog post about UUIDs in RedwoodJS and Prisma. Definitely check it out!)

Next, we create our table. The name of the table doesn’t matter, but Postgres may run into errors if you don’t wrap the table name in double quotes. The columns of your table should match what you have in your contact form (name, email, etc.). And, to avoid accidentally destroying any existing data, we tell Postgres to only create the table if it doesn’t exist.

### Add a Row

After the `messages` table is initialized, let’s execute an SQL command to create a new row in the `messages` table using the form data.

// create-row-action.ts

import { sql } from "@vercel/postgres";

type ValidFormData = {
 name: string;
 email: string;
 message: string;

export const createRow = async (data: ValidFormData) => {
 try {
   await sql`INSERT INTO "messages" (name, email, message) VALUES (${}, ${}, ${data.message});`;
   console.log("Successfully added row in messages table");
 } catch (error) {
   throw new Error(`Yikes! We ran into an error: ${error}`);

### All Together Now

With our table and row functions in place, we can now import and use them in server-action.ts. Basically, if the validations we created are successful, then create a messages table (if it doesn’t exist) and add a table row with the validated form data.

// server-action.ts

'use server'

import { redirect } from 'next/navigation'
import { z } from 'zod'

import { createTableIfNotExists } from './create-table-action'
import { createRow } from './create-row-action'

// ...

export default async function contactAction(_prevState: any, params: FormData) {
 const validation = schema.safeParse({
   name: params.get('name'),
   email: params.get('email'),
   message: params.get('message')

 if (validation.success) {
   await createTableIfNotExists().then(() => createRow(
 } else {
   return {
     errors: validation.error.issues

// ...

### Voilà

From here, you can manage your form data in the Vercel dashboard under the Storage tab. If all goes well, you should now see your form data populated in your database table.

Screenshot of the Mister Deejay application contact form

Screenshot of the Postgres data viewer in the Vercel dashboard

## Now Go Save some Data!

I hope that the examples and discussion in this post will help you lay the foundation for some fun use cases with Vercel Postgres and Next.js. If you’d like to learn more about how Next.js can supercharge your products, give us a shout. We can’t wait to chat!

Photo by Steve Johnson on Unsplash

Let's Chat

Are you ready to build something brilliant? We're ready to help.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
RedwoodJS Logo

for builders

Grants Pass, Oregon • September 26 - 29, 2023
View All