Build a simple code snippet manager with Neon’s serverless driver, Clerk, and Nextjs
Tags: neon, clerk, nextjs, typescript
Published: 2024-02-12 18:17:39

## 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](https://neon.tech/?ref=hm), 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](https://github.com/codewithmide/neon-snippetManager?utm_source=hackmamba&utm_medium=blog&utm_id=HMBcommunity), and you can also access the live view through this [link](https://neon-snippet-manager.vercel.app/?utm_source=hackmamba&utm_medium=blog&utm_id=HMBcommunity).

## Getting started with Clerk

Before diving into the user authentication process, let's take a quick moment to introduce [Clerk](https://clerk.com/docs?utm_source=hackmamba&utm_medium=blog&utm_id=HMBcommunity). 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](https://github.com/clerk/clerk-nextjs-app-quickstart?utm_source=hackmamba&utm_medium=blog&utm_id=HMBcommunity) 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](https://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:
![Fig. 1. Add application](https://paper-attachments.dropboxusercontent.com/s_990C55B289AEEA168877A8023FBB606E59629195DA7A5F510FAA930D7CC1CD49_1701002448535_Sign+up.PNG)

- Click on the **Add application** card, and you will be redirected to a page similar to the one below:
![Fig. 2. Create an application](https://paper-attachments.dropboxusercontent.com/s_990C55B289AEEA168877A8023FBB606E59629195DA7A5F510FAA930D7CC1CD49_1701002510587_app+creation.PNG)

- 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 existing `env.local.example` to `env.local`. API keys are highly sensitive: keep them secure and don’t expose them to the public!
![Fig. 3. API keys](https://paper-attachments.dropboxusercontent.com/s_990C55B289AEEA168877A8023FBB606E59629195DA7A5F510FAA930D7CC1CD49_1701002593535_env+files.PNG)

- 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:
![Fig. 4. Auth page](https://paper-attachments.dropboxusercontent.com/s_990C55B289AEEA168877A8023FBB606E59629195DA7A5F510FAA930D7CC1CD49_1700937360717_login+page+clerk.PNG)

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](https://neon.tech/?ref=hm) 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** **p****erformance**: Neon's cloud-based architecture ensures scalability and performance, enabling it to handle growing data volumes and complex queries without compromising on responsiveness.
- **Cost-****e****ffectiveness**: 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](https://console.neon.tech/sign_in?utm_source=hackmamba&utm_medium=blog&utm_id=HMBcommunity) 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.
![Fig. 5. Create a Neon project](https://paper-attachments.dropboxusercontent.com/s_990C55B289AEEA168877A8023FBB606E59629195DA7A5F510FAA930D7CC1CD49_1701002706619_neon-project.PNG)

- 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.
![Fig. 6. Connection string](https://paper-attachments.dropboxusercontent.com/s_990C55B289AEEA168877A8023FBB606E59629195DA7A5F510FAA930D7CC1CD49_1701002767958_connectionkey.PNG)

- Now, let's create a table to store your code snippets. Head over to the **SQL Editor** tab.
![Fig. 7. SQL editor](https://paper-attachments.dropboxusercontent.com/s_990C55B289AEEA168877A8023FBB606E59629195DA7A5F510FAA930D7CC1CD49_1701002834141_editor.PNG)

- Inside the editor, copy and paste the following SQL query:

```sql
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:
![Fig. 8. Create a Table](https://paper-attachments.dropboxusercontent.com/s_990C55B289AEEA168877A8023FBB606E59629195DA7A5F510FAA930D7CC1CD49_1701002878878_table+created.PNG)

- 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:
![Fig. 9. Created empty table](https://paper-attachments.dropboxusercontent.com/s_990C55B289AEEA168877A8023FBB606E59629195DA7A5F510FAA930D7CC1CD49_1701002942070_snippet+table.PNG)

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` and `SnippetList`, 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 called `snippets.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.

```javascript
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:

```javascript
    DATABASE_URL=postgres://[user]:[password]@[neon_hostname]/[database_name]
```

- Inside your `hooks` directory, create a custom hook file called `useSnippets.ts`, then copy and paste the below code inside:

```javascript
    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 function `setSnippets` to update this state variable.
- When the component mounts, the `useEffect` hook fetches code snippets from the database using the `getSnippets` function and updates the `snippets` state variable accordingly.</br>

</br>
</br>
- Add two components named `SnippetForm.tsx` and `SnippetList.tsx` into the `components` directory.

In the `SnippetForm.tsx` file, copy and paste the following code:

```javascript
    "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:

```javascript
    "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` and `SnippetList.tsx`, let's update the `page.tsx` file in your `app` directory. Copy and paste the code below:

```javascript
    "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:

![Fig. 10. Localhost:3000](https://paper-attachments.dropboxusercontent.com/s_990C55B289AEEA168877A8023FBB606E59629195DA7A5F510FAA930D7CC1CD49_1700997135524_demo.PNG)

When you check your table in the Neon console, you should see something similar as well:

![Fig. 11. Populated table](https://paper-attachments.dropboxusercontent.com/s_990C55B289AEEA168877A8023FBB606E59629195DA7A5F510FAA930D7CC1CD49_1700997220977_demo+table.PNG)

## 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](https://neon.tech/?ref=hm) and [Clerk](https://clerk.com/docs?utm_source=hackmamba&utm_medium=blog&utm_id=HMBcommunity) 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.