Build a simple code snippet manager with Neon’s serverless driver, Clerk, and Nextjs

Olumide Micheal - Feb 12 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode
  • After cloning, open the project in your preferred code editor and install the project dependencies by running
npm install
Enter fullscreen mode Exit fullscreen mode
  • 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:
    Fig. 1. Add application

  • Click on the Add application card, and you will be redirected to a page similar to the one below:
    Fig. 2. Create an application

  • 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

  • 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
Enter fullscreen mode Exit fullscreen mode

This should redirect you to a sign-up/sign-in page that looks similar to the one below:
Fig. 4. Auth page

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
Enter fullscreen mode Exit fullscreen mode
  • 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.
    Fig. 5. Create a Neon project

  • 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

  • Now, let's create a table to store your code snippets. Head over to the SQL Editor tab.
    Fig. 7. SQL editor

  • 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
    );
Enter fullscreen mode Exit fullscreen mode
  • 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

  • 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

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.
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);
      }
    };
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode
  • Inside your hooks directory, create a custom hook file called useSnippets.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 };
    }
Enter fullscreen mode Exit fullscreen mode
  • 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.
  • Add two components named SnippetForm.tsx and SnippetList.tsx into the components 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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:
    "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>
      )
    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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

Fig. 11. Populated table

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .