Knowledge Hub
Building full-stack web apps used to be a complex task. But then came Next.js, a game-changer for React developers like you and me. This powerful framework from Vercel makes full-stack development very easy.
Let's build a guestbook app together to see the process in action using the Vercel stack. Inspired by Lee Robinson's project, this article will be your step-by-step guide to craft your own personalized version in no time.
We'll start from the ground up, but to make things smooth, make sure you have some HTML, CSS, and JavaScript skills, along with a basic understanding of React concepts. Familiarity with Next.js (pages) is a bonus.
Check out the final demo here and grab the source code from this repository.
Building modern web apps became easier with Vercel development tools. The Vercel stack combines Next.js, its powerful storage, and lightning-fast hosting for a smooth and efficient development experience. We'll use "Next.js" for the app's front-end, and "Tailwind CSS" (a friendly CSS framework) for beautiful styling without the hassle. For the backend, Vercel's "serverless functions" will handle everything seamlessly. We'll connect it to their "PostgreSQL database" and host the whole app on "Vercel's cloud" to make it ready for your audience.
Back in 2016, Next.js was introduced as a simple open-source project for React developers. But it quickly grew into more than just a tool. It became a powerful all-in-one solution, taking React to the next level with server-side rendering and static site generation.
This helped in building full-stack web apps faster and easier, with some developer-friendly features like automatic image optimization, pre-rendering, and even file-based routing for serverless functions. By 2021, Next.js had become a popular JavaScript framework in the entire developer community.
Next.js moved from 11th place in 2022 to 6th in 2023. The developer experience sky-rocketed after Next.js released the awesome new features: the app router powered by React Server Components for better routing, and "server actions" add even more power. Together, they make Next.js one of the best frameworks out there for creating modern web apps.
You can easily create a new Next.js app by typing the npx create-next-app my-guestbook
command in your terminal. Don't worry if the questions seem confusing - just keep hitting Enter.
This will create a folder called my-guestbook
with everything you need. Open it in your favorite code editor and use the npm run dev
command to see the demo app.
As you build your guestbook, you'll need two extra packages: next-auth
and @vercel/postgres
. Install them using the commad below.
npm install next-auth @vercel/postgres
app
directoryStarting with Next.js 13, there's a new way to handle routes called the app
router. If you used Next.js before 2023, you might be familiar with the pages
router. Unlike pages, the app
directory uses "folders" for routing. For example, the /home
folder inside /app
becomes a route, and the file inside (like page.js
or page.tsx
) gets displayed when you visit that route.
In this architecture, every component is a React Server Component by default. This means you don't need functions like getServerSideProps
anymore for server-side rendering. If you want a component to render on the client instead, just add "use client
" at the beginning.
The app directory also lets you have nested and parallel routes, and use layout
components to share formatting across pages. To learn these features in more detail, visit the documentation here.
The page.tsx
and layout.tsx
files in the /app
directory are like the main entrance of our application. The layout.tsx
sets the overall layout for both the home page in page.tsx
and any other routes we add later. For now, let's focus on these files to build our guestbook. You can later add a separate route for this and even showcase your portfolio on the home page.
We do not need to change much in layout.tsx
, we'll just adjust the "metadata
" configuration to change the title of the app.
// /app/layout.tsx
export const metadata = {
title: "My Guestbook",
description: "Sign here to leave your mark on my page",
};
Open up the file page.tsx
. This is where we'll create the main page for our guestbook application. You'll see it's already styled using Tailwind CSS by default.
Now, navigate to page.tsx
. This is where we will build our application home. You can notice that it is styled using Tailwind CSS. We'll just tidy up this page a bit by removing things we don't need, then add our guestbook components.
// /app/page.tsx
import { Suspense } from "react";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center p-12">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Giridhar's Guestbook
</p>
</div>
<div className="relative w-full max-w-2xl my-12">
<h1 className="relative font-semibold dark:drop-shadow-[0_0_0.3rem_#ffffff70] text-2xl mb-8 tracking-tighter">
Sign my guestbook
</h1>
<Suspense fallback={<p>loading...</p>}>
<GuestbookForm />
{/* <GuestbookEntries /> */}
</Suspense>
</div>
</main>
);
}
We cleaned up the template, keeping only the header. You can add your name to the header. Below the header, I added a new section with a title (h1
) and a Suspense
component to handle loading forms. Suspense, in React, lets us display a placeholder while forms are fetching, then smoothly switch to the real thing when they're ready.
Now, let's build the two main parts of our app: the form for writing messages (GuestbookForm
) and the list to display messages (GuestbookEntries
).
Next.js works with React server components by default. If you need a client-side component, like a form, use the "use client" directive at the top of your component file.
// /app/form.tsx
"use client";
import { useRef } from "react";
import { useFormStatus } from "react-dom";
export default function Form() {
const formRef = useRef(null);
const { pending } = useFormStatus();
return (
<form
style={{ opacity: !pending ? 1 : 0.7 }}
className="relative max-w-[500px]"
ref={formRef}
>
<input
aria-label="Your message"
placeholder="Your message..."
disabled={pending}
name="entry"
type="text"
required
className="pl-4 pr-32 py-2 mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full border-neutral-300 rounded-md bg-gray-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100"
/>
<button
className="flex items-center justify-center absolute right-1 top-1 px-2 py-1 font-medium h-8 bg-neutral-200 dark:bg-neutral-700 text-neutral-900 dark:text-neutral-100 rounded w-16"
disabled={pending}
type="submit"
>
Sign
</button>
</form>
);
}
Before we render this form component, let's set things up by adding a login step to gather user information.
To handle logins in our Next.js app, let's use a reliable library called Next-Auth.js
. It takes the pain out of complex OAuth setups, making it easy to integrate your Next.js app with popular providers like Google, Facebook, and even GitHub - the developer's go-to platform. So, let's use GitHub to implement authentication.
Go to the developer settings, and create a new OAuth app to use for authentication.
Click "Generate new client secret" to generate a new client secret which you can use to connect your app using Next-auth.
Now, make a new .env
file and add these three things: "Client id", "Client secret" and "Auth secret". Next-auth uses these details to connect to your OAuth app and let users log in securely.
// .env
AUTH_SECRET=<any-string>
GITHUB_CLIENT_KEY=<your-OAuth-app-key>
GITHUB_CLIENT_SECRET=<your-OAuth-app-secret>
Inside the /lib
folder, create a file named auth.ts
. Let's define a helpful function called auth
that allows components to easily access session details.
// /lib/auth.ts
import { getServerSession } from "next-auth";
import GitHub from "next-auth/providers/github";
export const authOptions = {
secret: process.env.AUTH_SECRET,
providers: [
GitHub({
clientId: process.env.GITHUB_CLIENT_KEY as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
}),
],
} satisfies NextAuthOptions;
export function auth(...args) {
return getServerSession(...args, authOptions);
}
The authOptions
will contain the configuration of your app. You need to set the providers
you want to use in this object. In the auth
function, we're using Next-auth's getServerSession()
method to grab and return the details of the logged-in user.
Next-auth automatically creates routes for handling login
and logout
using a dynamic API route. You can create an API route using a folder, you need to use route.ts
, not page.ts
like in page routes. Inside the /api/auth
, create a dynamic route [...nextauth]
to handle authentication.
// /app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import { authOptions } from "@/lib/auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
To add Signin
and Signout
buttons, let's use the auth
method. Create a new file called buttons.js
inside the /app
directory and add the below code.
// /app/buttons.js
"use client";
import { signIn, signOut } from "next-auth/react";
export function SignOut() {
return (
<button
className="text-xs text-neutral-700 dark:text-neutral-300 mt-2 mb-6"
onClick={() => signOut()}
>
Sign out
</button>
);
}
export function SignIn() {
return (
<button
className="px-3 py-2 border border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800 rounded p-1 text-sm inline-flex items-center leading-4 text-neutral-900 dark:text-neutral-100 mb-8"
onClick={() => signIn("github")}
>
<div className="ml-3">Sign in with GitHub</div>
</button>
);
}
In this code, we are using the built-in signIn
and signOut
methods to implement "login" and "logout" functionality with on-button click. You should pass the provider inside the signIn()
method and Next-auth uses that to authenticate the user.
To keep our guestbook safe and secure, we only want people who are logged in to leave messages. So, before anyone can see the form to sign the book, we need to check if they're already logged in.
// /app/page.js
// ...
import { SignIn, SignOut } from "./buttons";
import { auth } from "@/lib/auth";
import Form from "./form";
export default function Home() {
/* ... */
}
async function GuestbookForm() {
const session = await auth();
return session ? (
<>
<Form />
<SignOut />
</>
) : (
<SignIn />
);
}
Here, we check for the session
first. If the user has not signed in yet, we'll display the SignIn
button, else, we'll render the Form
component.
We'll store our messages in a serverless PostgreSQL database provided by Vercel. Go to the Vercel dashboard and log in to your account (I recommend logging-in using GitHub). Once you're in, click "Add new" and choose "Storage."
Click the "Create" button next to the "Postgres - serverless SQL" option. Fill in the details and choose a region near your users. This helps a little in reducing latency.
Vercel makes it easy to connect to your database with their @vercel/postgres
package. Grab the database info from .env.local
tab and add it in the .env
file. Then, import the sql
method to run your SQL statements within the Next.js app.
Once we have our database up and running, let's build a table for keeping track of all the guest entries. You can run the SQL statements right here in the "Query" section under "Data". To keep things simple, we'll use a few essential fields. Type the SQL statement below into the editor and click "Run Query" to make the table.
You can now write serverless functions right inside your components with Next.js "server actions". These server actions are integrated with the caching and revalidation architecture of Next.js, making data mutations very easy to implement. For more information, refer to the documentation.
There are two ways to use server actions:
// /app/actions.js
"use server";
import { auth } from "@/lib/auth";
import { sql } from "@vercel/postgres";
import { revalidatePath } from "next/cache";
export async function saveGuestbookEntry(formData) {
const session = await auth();
console.log({ session });
if (!session) {
throw new Error("Unauthorised");
}
const email = session.user?.email as string;
const created_by = session.user?.name as string;
const entry = formData.get("entry")?.toString() || "";
const body = entry.slice(0, 500);
await sql`INSERT INTO "Guestbook" (email, created_by, body, last_modified) VALUES (${email}, ${created_by}, ${body}, ${new Date().toISOString()});`;
revalidatePath("/");
}
The saveGuestbookEntry
function takes the information you enter in the form (formData
) and creates a new entry in the "Guestbook" table. It uses the name of each input field to grab the specific value. The sql
directive of @vercel/postgres
runs the SQL query on the database.
Then, we are going to use the revalidatePath
method of Next cache architecture to refresh the path smoothly. You can now import this method into the Form
component and use it in the form action.
// /app/form.tsx
// ...
import { saveGuestbookEntry } from "./actions";
export default function Form() {
// ...
return (
<form
action={async (formData) => {
await saveGuestbookEntry(formData);
formRef.current?.reset();
}}
>
{/* all existing code */}
</form>
);
}
Passing the server action
in the action prop of the <form>
element makes it easier to submit the form even when the client-side JavaScript is not completely loaded. For more information, refer to the React documentation.
Run npm run dev
to start the server and log in with your GitHub. Then, add some entries using the form. You'll find them in the "Browse" tab under the "Data" section in the Vercel dashboard.
You can run the SQL command from the Next.js app and display all the data in the page. Uncomment the <GuestbookEntries />
in page.tsx
and create this component down after the existing components in the file.
// /app/page.tsx
// ...
import { sql } from "@vercel/postgres";
export default function Home() {
return (
<main>
<div>
{/* all before code*/}
<Suspense fallback={<p>loading...</p>}>
<GuestbookForm />
<GuestbookEntries />
</Suspense>
</div>
</main>
);
}
async function GuestbookForm() {
/* ... */
}
async function GuestbookEntries() {
const { rows } =
await sql`SELECT * from "Guestbook" ORDER BY last_modified DESC;`;
return rows.map((entry) => (
<div key={entry.id} className="flex flex-col space-y-1 mb-4">
<div className="w-full text-sm break-words">
<span className="text-neutral-600 dark:text-neutral-400 mr-1">
{entry.created_by}:
</span>
{entry.body}
</div>
</div>
));
}
In the GuestbookEntries
component, we first fetch all the entries from the database. The <Suspense>
component will first display the fallback
(loading) UI while the entries are loading. Once the entries are ready, we instantly display them on the page along with the form for adding new entries.
Fill out the form and add your entries. You'll see them appear right away, no need to refresh. If anything goes wrong, check out the source code or use Google to troubleshoot.
Once you're happy with your code, commit and push it to a GitHub repository. Then, let's deploy it to the world. Vercel offers a free 'Hobby' plan for deploying Next.js apps. Go to the link: vercel.com/new. If you've already connected your GitHub account, you'll see your code ready to be deployed.
"Import" the project from the list and paste the contents of your .env
file in the "Environment variables" tab. Finally, click on "Deploy" to see your app live in a few minutes.
This app can be your wedding guestbook, a feedback hub for your business, or just a fun way to connect with friends. Try mine here and share your thoughts.
In conclusion, the full-"Vercel" stack provides a developer-friendly way to easily create full-stack web applications. It provides a full set of tools you need - Next.js for frontend, Serverless functions and Storage for backend, and easy cloud hosting for deployment. Next.js, with its new features like 'app router' and 'server actions,' simplifies the implementation of server rendering using React server components.
The Vercel serverless functions and PostgreSQL database provide a simple way to build a scalable backend for your apps. You can also deploy your app to the internet using Vercel's free hobby plan. Finally, Vercel takes the complexity out of full-stack development, letting you focus on what matters - creating amazing apps!