Introduction
Ever feel like you are spending hours searching through mountains of code just to find that one perfect snippet? We've all been there. But what if there was a better way?
In this hands-on tutorial, you'll discover the ease and versatility of Neon, not just as a database but as a comprehensive solution that simplifies the development process. We’ll also showcase Clerk, a powerful user authentication platform that seamlessly integrates with Neon and Next.js.
Prerequisites
You’ll need a few things to follow along:
- Basic knowledge of React.js/Next.js
- Basic knowledge of Typescript
- Basic understanding of how databases work
Learning objectives
By the end of this article, you’ll have learned how to do the following:
- Set up user authentication using Clerk
- Supercharge your app with Neon's lightning-fast data storage
- Effortlessly store and retrieve data on Neon from your Next.js application.
Project demo
The tutorial code is available here, and you can also access the live view through this link.
Getting started with Clerk
Before diving into the user authentication process, let's take a quick moment to introduce Clerk. Clerk is a user management platform that simplifies adding secure authentication and user management features to your web applications. It provides comprehensive tools for managing user registration, login, profiles, and permissions. With Clerk, you can quickly add robust authentication to your applications without the hassle of building and maintaining your authentication system.
Let's kick things off by setting up user authentication using Clerk. Clerk offers various approaches to user authentication, but for simplicity, we will opt for a predefined template offered by Clerk. This template provides a ready-made authentication flow that can be easily integrated into your application.
- Open your command-line interface or CLI, and clone this repository by running the following command:
git clone https://github.com/clerk/clerk-nextjs-app-quickstart
- After cloning, open the project in your preferred code editor and install the project dependencies by running
npm install
Once that's done, create a Clerk account by navigating to this URL: dashboard.clerk.com.
Here, you will create an application, configure how users are authenticated, and get the API keys for your project.After signing up, you should be directed to a screen similar to this:
Click on the Add application card, and you will be redirected to a page similar to the one below:
Include an application name and your preferred user authentication method. We suggest using the email address method for this example, as it offers greater accessibility than others. Afterward, click the Create application button. Upon successful creation, you will be redirected to your dashboard.
Locate your API keys at the bottom right of the screen, then copy and paste them into your
env.local
file locally. Rename the existingenv.local.example
toenv.local
. API keys are highly sensitive: keep them secure and don’t expose them to the public!
After pasting your API keys inside your
env.local
file, you can now run the project on localhost:3000 using the following command:
npm run dev
This should redirect you to a sign-up/sign-in page that looks similar to the one below:
After successfully signing in, you will be redirected to an empty page.
Now, you have successfully set up the user authentication for your app! Let's proceed to the next step.
Getting started with Neon
Before embarking on our journey with Neon, let's take a moment to introduce this next-gen Postgres database. Neon is a serverless Postgres built for the cloud. It separates compute and storage to offer modern developer features such as autoscaling, branching, bottomless storage, and more.
But why choose Neon over other Postgres database providers? Here are a few compelling reasons:
- Standard Postgres: Neon isn’t another “Postgres-compatible” database. This means all existing Postgres libraries or drivers work, and all Postgres syntax works.
- Scalability and performance: Neon's cloud-based architecture ensures scalability and performance, enabling it to handle growing data volumes and complex queries without compromising on responsiveness.
- Cost-effectiveness: Neon's pricing model is designed to be cost-effective, providing flexible plans to suit various project sizes and budgets.
Now that you're familiar with Neon's capabilities and advantages, let's dive into the world of Neon, where data management becomes a breeze! In this section, you will install Neon, create a project, get your connection string, and create a database table.
- To get started, install the Neon serverless driver into the project by running this command:
npm install @neondatabase/serverless
Once the installation is complete, create a Neon account and sign in to the console to start a new project.
In the console, you will see a screen similar to the one below. Enter your desired Project name and Database name, and then click the "create project" button at the bottom of the page.
A connection string similar to the one below will be provided. Make sure to copy and save it somewhere safe for later use in the project.
Now, let's create a table to store your code snippets. Head over to the SQL Editor tab.
Inside the editor, copy and paste the following SQL query:
CREATE TABLE snippets (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
snippet TEXT NOT NULL,
user_id VARCHAR(255) NOT NULL
);
Once you click the run button, your table should be created in seconds. A successful message should appear in your SQL Editor like this:
Now that your table is created, head to the "Tables" tab to confirm. At this point, it will be empty, and it should look similar to the one I have below:
Well done! You have successfully integrated Neon into your codebase, established a project, obtained your connection string, and created a database table. Let us move on to the next step.
Project development
Before diving into the code, let's organize our project by creating the following folders within the src directory:
-
components: This folder will house all our reusable React components, such as
SnippetForm
andSnippetList
, responsible for the application's UI elements and functionalities. -
hooks: This folder will hold custom React hooks like
useSnippets
, which manages the application's state and logic related to snippets. - lib: This folder will serve as a central location for utility functions and services required throughout the application, including functions for fetching, adding, and deleting snippets.
By organizing our code this way, we can maintain a clean and modular structure, improving maintainability and reusability.
-
Create a file to handle Neon interactions
Inside your
lib
directory, create a file calledsnippets.ts
. This file will be the intermediary between your application and the Neon database, handling all data communication. The code within this file will allow you to fetch, add, and delete snippets from the database using the user id linked to the Clerk account.
import { neon } from '@neondatabase/serverless';
const databaseUrl = process.env.DATABASE_URL;
export const getSnippets = async (user_id: string | null) => {
if (!databaseUrl) {
console.error("DATABASE_URL is not defined.");
return [];
}
const sql = neon(databaseUrl);
try {
const response = await sql`SELECT * FROM snippets WHERE user_id = ${user_id || ''} ORDER BY id DESC`;
return response;
} catch (error) {
console.error("Error fetching snippets:", error);
return [];
}
};
export const addSnippet = async (title: string, snippet: string, user_id: string) => {
if (!databaseUrl) {
console.error("DATABASE_URL is not defined.");
return null;
}
const sql = neon(databaseUrl);
const response = await sql`INSERT INTO snippets (title, snippet, user_id) VALUES (${title}, ${snippet}, ${user_id}) RETURNING *`;
return response;
};
export const deleteSnippet = async (id: number) => {
if (!databaseUrl) {
console.error("DATABASE_URL is not defined.");
return;
}
const sql = neon(databaseUrl);
try {
const response = await sql`DELETE FROM snippets WHERE id = ${id}`;
return response;
} catch (error) {
console.error("Error deleting snippet:", error);
}
};
On line 3, ensure to make reference to the database URL from your .env.local
file with the one you got earlier from Fig. 6. Also, note that your database URL should be in the format below:
DATABASE_URL=postgres://[user]:[password]@[neon_hostname]/[database_name]
- Inside your
hooks
directory, create a custom hook file calleduseSnippets.ts
, then copy and paste the below code inside:
import { useEffect, useState, SetStateAction, Dispatch } from 'react';
import { getSnippets } from '@/lib/snippets';
import { useUser } from "@clerk/nextjs";
type Snippet = Record<string, any>;
export function useSnippets() {
const [snippets, setSnippets] = useState<Record<string, any>[]>([]);
const setSnippetsState: Dispatch<SetStateAction<Record<string, any>[]>> = setSnippets;
const [userId, setUserId] = useState<string | null>(null);
const user = useUser();
useEffect(() => {
setUserId(user?.user?.id || null);
}, [user]);
useEffect(() => {
const fetchData = async () => {
try {
const data = await getSnippets(userId);
setSnippets(data || []);
} catch (error) {
console.error('Failed to fetch snippets:', error);
}
};
fetchData();
}, [userId, setSnippets]);
return { snippets, setSnippets: setSnippetsState };
}
- This code snippet defines a custom React hook called
useSnippets
for managing code snippets. It utilizes the React state and effect system to fetch and manage code snippets from the Neon-powered database. - The hook maintains an internal state variable
snippets
, which is an array of code snippet objects, and provides a functionsetSnippets
to update this state variable. - When the component mounts, the
useEffect
hook fetches code snippets from the database using thegetSnippets
function and updates thesnippets
state variable accordingly.
- Add two components named
SnippetForm.tsx
andSnippetList.tsx
into thecomponents
directory.
In the SnippetForm.tsx
file, copy and paste the following code:
"use client"
import React, { useState, useEffect } from 'react';
import { useUser } from "@clerk/nextjs";
const SnippetForm = ({ onAddSnippet }: { onAddSnippet: any }) => {
const [title, setTitle] = useState('');
const [snippet, setsnippet] = useState('');
const [userId, setUserId] = useState<string | null>(null);
const user = useUser();
useEffect(() => {
setUserId(user?.user?.id || null);
}, [user]);
const handleSubmit = async (e: any) => {
e.preventDefault();
if (!title || !snippet || !userId) {
alert("Title, snippet, and user ID are required!");
return;
}
try {
await onAddSnippet(title, snippet, userId);
setTitle('');
setsnippet('');
} catch (error) {
console.error("Failed to add snippet:", error);
}
};
return (
<div>
<form onSubmit={handleSubmit} className="flex flex-col items-start gap-4">
<input
type="text"
name="title"
id="title"
placeholder="Enter your snippet title"
className="w-full rounded-md p-3 text-black"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<textarea
name="snippet"
id="snippet"
rows={6}
placeholder="Paste your code snippet"
className="rounded-md w-full p-3 text-black"
value={snippet}
onChange={(e) => setsnippet(e.target.value)}
/>
<button type="submit" className="bg-white px-8 py-3 text-black font-semibold rounded-md">
Save Snippet
</button>
</form>
</div>
);
};
export default SnippetForm;
The SnippetForm
component displays a form to add new code snippets. It manages the form's input fields for the snippet title and code using the local state. When the form is submitted, it triggers an asynchronous function to add the new snippet to the database.
Now, in your SnippetList.tsx
file, copy and paste the following code:
"use client"
import React from 'react';
const SnippetList = ({ snippets, onDelete }: { snippets: any, onDelete: (id: number) => void }) => {
// Function to copy the snippet to the clipboard
const copyToClipboard = (snippet: string) => {
navigator.clipboard.writeText(snippet).then(() => {
alert("Copied");
});
};
// Function to handle deleting a snippet
const handleDelete = async (id: number) => {
try {
await onDelete(id);
} catch (error) {
console.error("Failed to delete snippet:", error);
}
};
return (
<div className="space-y-4 w-full">
{snippets.map((snippet: any, id: number) => (
<div key={id} className="bg-white text-black p-4 rounded shadow-md w-full">
<p className="font-bold mb-2">{snippet.title}</p>
<pre className="whitespace-pre-wrap text-sm bg-black text-white rounded p-3 mb-2">
{snippet.snippet}
</pre>
<div className='flex items-center justify-between'>
<button
onClick={() => copyToClipboard(snippet.snippet)}
className="text-sm bg-gray-200 hover:bg-gray-300 rounded p-2 mr-2"
>
Copy
</button>
<button
onClick={() => handleDelete(snippet.id)}
className="text-sm bg-red-800 hover:bg-red-900 text-white rounded p-2"
>
Delete
</button>
</div>
</div>
))}
</div>
);
};
export default SnippetList;
The SnippetList
component renders a list of code snippets. It receives an array of snippets and a callback function for deleting snippets. It maps through the snippets array, displaying each snippet's title and code. Users can copy individual snippets to the clipboard or delete them using the provided buttons.
- Now that you have created the
SnippetForm.tsx
andSnippetList.tsx
, let's update thepage.tsx
file in yourapp
directory. Copy and paste the code below:
"use client"
import { UserButton } from "@clerk/nextjs";
import { useEffect, useState } from "react";
import { getSnippets, addSnippet, deleteSnippet } from "@/lib/snippets";
import SnippetForm from "@/components/snippetForm";
import SnippetList from "@/components/snippetList";
import { useSnippets } from "@/hooks/useSnippets";
import { useUser } from "@clerk/nextjs";
export default function Home() {
const user = useUser();
const { snippets, setSnippets } = useSnippets();
const fetchData = async () => {
try {
const user_id = user?.user?.id || null;
const data = await getSnippets(user_id);
setSnippets(data || []);
} catch (error) {
console.error('Failed to fetch snippets:', error);
}
};
useEffect(() => {
fetchData();
}, [setSnippets, fetchData]);
const handleAddSnippet = async (title: string, snippet: string, userId: string) => {
try {
const response = await addSnippet(title, snippet, userId);
if (response) {
setSnippets((prevSnippets) => [...response, ...prevSnippets]);
} else {
return null;
}
} catch (error) {
console.error("Failed to add snippet:", error);
};
};
const handleDeleteSnippet = async (id: number) => {
try {
await deleteSnippet(id);
setSnippets(snippets.filter(snippet => snippet.id !== id));
} catch (error) {
console.error("Failed to delete snippet:", error);
}
};
return (
<div className="w-full min-h-screen font-bold font-sansSerif bg-black overflow-x-hidden flex-col flex items-center gap-16">
{/* Header */}
<nav className="w-11/12 border-white border-b py-6 flex items-center justify-between">
<div className="tracking-wide text-xl font-semibold">SnippetHive</div>
<div className="flex items-center gap-3">
<UserButton afterSignOutUrl="/" />
</div>
</nav>
{/* Header Ends */}
{/* Body */}
<main className="flex justify-between w-11/12">
<div className="w-1/2 border-r border-white flex-col flex gap-6 pr-6">
<h1 className="text-2xl">
Save a code snippet
</h1>
<div>
<SnippetForm onAddSnippet={handleAddSnippet} />
</div>
</div>
<div className="w-1/2 flex-col min-h-max flex items-start gap-6 pl-6">
<h1 className="text-2xl">
My snippet(s)
</h1>
<SnippetList onDelete={handleDeleteSnippet} snippets={snippets} />
</div>
</main>
{/* Body Ends */}
</div>
)
}
This code snippet above defines the Home
component, which serves as the main page of the SnippetHive application. It integrates user authentication, snippet management, and data fetching using Clerk and Neon. The component renders a layout with a header, a main section, and a form for adding snippets.
- Run the code on localhost:3000 to see the changes take effect:
npm run dev
Now that you have successfully built a Neon-powered application, let’s add a code snippet to test. Once added, refresh your browser to see the result!
You should have a screen similar to the one below:
When you check your table in the Neon console, you should see something similar as well:
Conclusion
Congratulations! You successfully deployed SnippetHive, a dynamic code snippet manager built using Neon's serverless driver, Clerk, and Next.js. Throughout this journey, you've delved into key concepts like user authentication, fast data storage, and seamless integration with a Next.js application.
By completing this project, you've gained valuable knowledge about utilizing powerful tools like Neon and Clerk and refined your skills in building practical applications. This is just a starter guide; however, feel free to customize and expand SnippetHive to suit your specific needs. Additionally, you’re now equipped to explore additional features such as editing an existing code snippet, generating links to code snippets, or integrating other tools to enhance its capabilities further.
Top comments (2)
Fantastic tutorial, and just what I was looking for. I've been really wanting to prototype with Clerk, but have been annoyed with how it and Supabase fight with eachother. +1!
really eager to discover what you're prototyping with Clerk, Dylan!
out of curiosity -- what do you find frustrating? Clerk and Supabase work great together in many cases. if you're using Supabase for your database needs, you can use Clerk for auth.
see how both works together here: Integrate Supabase with Clerk
hope it helps! please keep us posted on what you're building :)