Build a Code Snippet Web App with Next.js and FaunaDB

Share this article

Build a Code Snippet Web App with Next.js and FaunaDB

While programming, developers encounter problems requiring code reuse, leading to repetitive programming that can be time-wasting and reduces productivity. This gives rise to the need for reusable source code called “code snippets”. These snippets prevent repetitive code during programming, can be saved for future use, and are sharable.

In this tutorial, we’ll build a website to help users save daily code snippets using the Next.js web development framework, and powered by the Fauna database to handle the storage, manipulation, and display of code snippets. By working on this fun project, we’ll also learn how to create a basic CRUD app with Next.js and FaunaDB that could also be used for building other similar projects.

A working version of this project can be found at GitHub. To follow along, you’ll need Node installed on your machine, as well as a FaunaDB account and a Google account (for authentication).

Key Takeaways

  • Utilize Next.js and FaunaDB to build a dynamic code snippet web application that allows for CRUD operations and user authentication.
  • Install necessary dependencies using npm, including FaunaDB for database management and SWR for data fetching.
  • Set up FaunaDB to create a database and collections to store code snippets, and generate a secret key for secure access.
  • Implement CRUD functionalities: Create, Read, Update, and Delete code snippets through the web interface, with added syntax highlighting and copy functionality.
  • Configure user authentication using NextAuth and Google Provider to ensure that only authorized users can create, edit, or delete snippets.
  • Optimize the application by leveraging Next.js features like server-side rendering and static generation, alongside FaunaDB’s indexing and query optimization.
  • Ensure robust security and error handling within the app, using FaunaDB’s built-in security features and Next.js error handling capabilities.

Installing Dependencies

In this section, we’ll look at how to install Next.js using the npx create-next-app command. This will initialize the Next CLI and build a new Next.js application.

We’ll also install the dependencies we’ll be using for the back end — FaunaDB and SWR — via the command line. SWR (state-while-revalidate) is a Next.js hook for fetching data. We’ll go into this in depth later in this tutorial.

Next.js installation

To install Next.js, enter the following command in the CLI:

npx create-next-app snippetapp

The above command creates a project directory called snippetapp with the Next.js starter template, which contains the required files for coding with Next. When Next has finished installing, change into the newly created directory:

cd snippetapp

FaunaDB and SWR installation

To install Fauna, we’ll be using the following command in the CLI:

npm install --save faunadb

Then to install SWR:

npm install swr@0.3.8

With this, we’ve installed all the dependencies we’ll be using to build our application and can now proceed to set up our database on Fauna.

FaunaDB Setup

FaunaDB is a serverless, real-time database. It transforms a traditional database into a flexible data API that still retains the capabilities of a database and its performance while delivering secure and scalable access to app data.

Here, we’ll create a user account and set up the database to store the data we’ll be using in our snippet application.

Creating a user account

To create a user account, navigate to the Fauna signup page and create an account.

Creating a FaunaDB account

After creating a user account, you’ll be redirected to the dashboard.

The FaunaDB dashboard

Create the snippet database and collection

Here, we’ll create a database with the collections required to manage the code snippets of our application. Click on CREATE DATABASE. We’ll create a database called snippets.

Creating a FaunaDB snippets database

On the new page that opens, click on NEW COLLECTION and create a collection called codesnippet.

collection page

After creating a collection, we get a page where we can create a document.

Create a document

Here, you’ll click on NEW DOCUMENT. A JSON document will open, where you can enter the details, as pictured below.

Document creation

{
  name: "Prompt User",
  description: "prompts the user",
  language: "javascript",
  code: "prompt('would you like to continue')"
}

Here, we define a snippet with attributes: name, description, language and code. Click on SAVE to save the new collection. We’ve successfully added a snippet to our database. Now we can proceed to getting our access credentials to use on our app.

Secret key creation

On the dashboard, click on Security. This opens up a new page to create our security key.

Creating a FaunaDB security key

Here, we’ll set the role to “server” instead of “admin”, and you can give the key a name. Click on the SAVE button to generate your key.

Creating a .env file

We’ll now create a .env file within the directory of our project. This file will store our generated secret key. In the .env file we have this:

FAUNA_SECRET = paste your key here

Creating a Code Snippet Page

In this section, we’ll build the display and upload page for the snippets, and also add functionality to it.

Open the project directory in your code editor and navigate to the index.js file in your pages folder. Here we’ll clear out the code and start building our application:

import Head from "next/head"
import Image from "next/image"
import styles from "../styles/Home.module.css"

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>View Snippet</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>Re-usuable Code Snippets</h1>
        <p className={styles.info}>Add your code snippets here...</p>
        <button>Create new snippet</button>
      </main>
    </div>
  )
}

Creating our Components

Now we’ll create a component file that will render our snippets. Create a folder named component in your working directory and create a file named Snippets.js within it with the following code:

import React from "react"
import styles from "../styles/Home.module.css"

function Snippets() {
  return (
    <div className={styles.cont}>
      <p className={styles.lang}>language</p>
      <h3 className={styles.name}>name of snippet</h3>
      <p className={styles.descp}>description of snippet</p>
      {/* Code will be displayed here*/}
      <div className={styles.links}>
        <a>Edit</a>
        <a>Delete</a>
      </div>
    </div>
  )
}

export default Snippets

Importing our components into the app

We’ll now add imports for this file in our index.js:

import Snippets from "../components/Snippets"

And use it in our app:

<button>Create new snippet</button>
<Snippets/>

Styling our app

We can now style our page. Navigate to the Home.module.css file in the styles folder and replace the styles there with the following:

.container{
  display: flex;
  height: 100%;
  min-height: 100vh;
  background: rgb(48, 48, 255);
  flex-direction: column;
  align-items: center;
  color: #fff;
  font-family: Montserrat;
}
.cont{
  color: #333;
  margin-top: 5px;
  background: rgb(214, 214, 214);
  border-radius: 15px;
  padding: 10px 15px;
}
.main button{
  width: fit-content;
  flex-grow: unset;
  display: inline-block;
  padding: 5px 10px;
  outline: none;
  border: none;
  border-radius: 5%;
  font-weight: bold;
  color: rgb(48, 48, 255);
}
.main button:hover{
  cursor: pointer;
}
.links{
  margin-top: 10px;
}
.links a{
  margin-left: 5px;
}
.links a:hover{
  cursor: pointer;
}

Viewing Our App

At this point, you should be able to start the dev server with npm run dev, visit http://localhost:3000, and see the skeleton of our app.

Setting up the Snippet Display Area

Next, we’ll create the display section for the snippet code. Create a new file called Code.js in the components folder and import it into Snippets.js:

import React from 'react'
import styles from '../styles/Home.module.css'
import Code from "./Code";

function Snippets() {
  return (
    <div className={styles.cont}>
      <p className={styles.lang}>language</p>
      <h3 className={styles.name}>name of snippet</h3>
      <p className={styles.descp}>description of snippet</p>

      {/* Code will be displayed here*/}
      <Code />

      <div className={styles.links}>
        <a>Edit</a>
        <a>Delete</a>
      </div>
    </div>
  )
}

export default Snippets

For the syntax highlighting of the code, we’ll be using two packages, namely react-syntax-highlighter and react-copy-to-clipboard. We can download this via the CLI:

npm install react-syntax-highlighter react-copy-to-clipboard --save

Then in Code.js:

import React from "react"
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"
import {atomDark} from "react-syntax-highlighter/dist/cjs/styles/prism"
import { CopyToClipboard } from "react-copy-to-clipboard"
import styles from "../styles/Home.module.css"
function Code() {
  const codeString = "npm install import react from 'react'"
  const [show, setshow] = React.useState(false)
  return (
    <div>
      <button onClick={() => setshow(!show)}>Show Code</button>
      {show ? (
        <div>
          <CopyToClipboard text={codeString}>
            <button className={styles.btn}>Copy</button>
          </CopyToClipboard>
          <SyntaxHighlighter language="javascript" style={atomDark}>
            {codeString}
          </SyntaxHighlighter>
        </div>
      ) : null}
    </div>
  )
}
export default Code

Here, we created a component to display code with syntax highlighting. We’ve also added copy and toggle-show functionality. Now in the styles file:

.btn{
  left: 80%;
  position: relative;
}

Testing the Code Blocks

To view this change, you can run npm run dev in the command line and view it in your browser. We have the string “npm install import react from ‘react'” displayed with syntax highlighting as a code block. There’s also a button to hide and display the code snippet, and a button that allows us to copy the code from the code block.

FaunaDB Initialization

In this section, we’ll fetch data from our FaunaDB database to our app. Create a file called Fauna.js in your project directory:

const faunadb = require("faunadb")
const faunaClient = new faunadb.Client({
  secret: process.env.FAUNA_SECRET
})
const q = faunadb.query
const getResponse = async () => {
  const { data } = await faunaClient.query(
    q.Map(
      q.Paginate(q.Documents(q.Collection("codesnippet"))),
      q.Lambda("doc", q.Get(q.Var("doc")))
    )
  )
  const snippets = data.map((snippet) => {
    snippet.id = snippet.ref.id
    delete snippet.ref
    return snippet
  })
  return snippets
}

module.exports = {
  getResponse,
}

Here, we’ve initialized FaunaDB with our secret key. We’ve also set up an async request to query our collection and return the data. We’ve stored the returned data in a variable named snippets, and deleted the ref to better structure the data. Other functionalities for creating, updating and deleting snippets will be added later in this tutorial.

Note that, if you’re getting an unauthorized error in the console, you may need to specify the domain name of the target endpoint. The default is db.fauna.com, but since the introduction of Region Groups, three cloud domains are available. Use the correct domain for your database’s Region Group:

  • Classic (US and EU): db.fauna.com
  • United States (US): db.us.fauna.com
  • Europe (EU): db.eu.fauna.com

Example code:

const faunaClient = new faunadb.Client({
  secret: process.env.FAUNA_SECRET,
  domain: "db.eu.fauna.com"
})

Handling our API requests

We’ll also create a file to handle our API request for our database. Within the api folder in pages, create a file called snippets.js with the following code:

import { getResponse } from "../../Fauna.js"
export default async function handler(req, res) {
  console.log(req)
  if (req.method !== "GET") {
    return res.status(405)
  }
  try {
    const snippets = await getResponse()
    return res.status(200).json(snippets)
  } catch (err) {
    console.log(err)
      res.status(500).json({ msg: "Something went wrong." })
  }
}

Above, we’ve simply set up a function to handle requests from our database. The snippets are returned as Json and will log errors if any occur. In Next.js, any file stored in the api folder is treated as API endpoints rather than a page and is rendered server-side.

What is SWR?

As said earlier, SWR (state-while-revalidate) is a Next.js hook for fetching data. It’s a perfect solution for fetching frequently updating data and is a good fit for our app.

Setting up SWR

We’ll use this to fetch data from FaunaDB. To use this, we need to import it into index.js:

import useSWR from "swr"

export default function Home() {
  const { data:snippets, mutate }=useSWR("api/snippets")
  ...
})

Here, we’ve imported SWR and used it to fetch data as set up in snippets.js. We’ve then stored these snippets in the snippets variable and will output them from there. We’ll now pass the snippets to our Snippets component to display:

- <Snippets />

+ {snippets &&
+   snippets.map((snippet) => (
+     <Snippets
+       key={snippet.id}
+       snippet={snippet}
+       snippetDeleted={mutate}
+     />
+   ))
+ }

Above, we’ve passed the key and snippet to Snippets. We’ve also set up a mutate property to update (re-fetch) snippets when a snippet is deleted. To use the passed data, we modify the Snippets component with the following:

function Snippets({snippet}) {
  return (
    <div className={styles.cont}>
      <p className={styles.lang}>{snippet.data.language}</p>
      <h3 className={styles.name}>{snippet.data.name}</h3>
      <p className={styles.descp}>{snippet.data.description}</p>

      <Code snippet={snippet}/>

      <div className={styles.links}>
        <a>Edit</a>
        <a>Delete</a>
      </div>
    </div>
  )
}

Above, we’ve inserted the snippet language, name and description received from FaunaDB in our code. To get the code from Fauna in our app, we’ve also had to pass the snippet prop down to the Code component.

Then in the Code component:

function Code({snippet}){
  ...
  <div>
    <CopyToClipboard text={snippet.data.code}>
      <button className={styles.btn}>Copy</button>
    </CopyToClipboard>
    <SyntaxHighlighter language="javascript" style={atomDark}>
      {snippet.data.code}
    </SyntaxHighlighter>
  </div>
  ...
}

We’re now done with the GetSnippet functionality. If we return to FaunaDB and create a new snippet, we see what’s pictured below.

added another document to FaunaDB

{
  "name": "console.log()",
  "language": "javascript",
  "description": "logs out data",
  "code": "console.log('Hello, world!')"'
}

Running the Code

To run in the CLI:

npm run dev

If you open the page in your browser, you’ll have a result similar to the image below.

Snippet display page

We’ve successfully created a snippet display page with functionalities to show and hide the code and copy the code snippet.

The Snippet Upload Page

We’ll need to create a link to the upload page from our home component. Next.js has provisions that make routing easier without you having to install react-router and other dependencies as you would if using native React code.

In index.js, we’ll import the Link module from next:

import Link from "next/link"

Then add it to our Create new snippet button:

- <button>Create new snippet</button>

+ <Link href="/upload">
+   <button>Create new snippet</button>
+ </Link>

We’ll create a new page in our pages folder and name it upload.js.

Back in our Fauna.js file, we’ll create and also export a function to create snippets in our app:

const createSnippet = async (code, language, description, name) => {
  return await faunaClient.query(q.Create(q.Collection("codesnippet"), {
    data:{code, language, description, name}
  }))
}

module.exports = {
  getResponse,
  createSnippet,
}

Here, we’ve created the function createSnippet, which will take in some parameters and pass them as data in the new document that will be created in the database.

Adding a function to create snippets

We’ll also configure our endpoint to create snippets. Create a new file called createSnippet.js in the api folder and populate it with the following code:

import { createSnippet } from "../../Fauna"

export default async function handler(req, res) {
  const { code, language, description, name } = req.body
  if (req.method !== "POST") {
    return res.status(405).json({msg:"unauthorized"})
  }
  try {
    const createdSnippet = await createSnippet(code, language, description, name)
    return res.status(200).json(createdSnippet)
  } catch (error) {
    console.log(error)
    res.status(500).json({msg:"unauthorized"})
  }
}

Creating our upload page

We’ll now create the upload page in our upload.js file. For our form to create snippets, we’ll be using the react-hook-form. We’ll install this via the CLI:

npm install react-hook-form

Then, in our upload.js file:

import React from "react"
import { useForm } from "react-hook-form"
import { useRouter } from "next/router"
import style from "../styles/form.module.css"
import { Link } from "next/link"

function upload({ snippet }) {
  const { register, handleSubmit, errors, reset } = useForm()
  const router = useRouter()
  const createSnippet = async (data) => {
    const { code, language, description, name } = data
    console.log(data)
    try {
      // code here to push to Fauna
    } catch (error) {
      console.log(error)
    }
  }
  return (
    <div className={style.cont}>
      <form
        className={style.form}
        onSubmit={handleSubmit(snippet ? updateSnippet : createSnippet)}
      >
        <div>
          <label htmlFor="name">Name</label>
          <input
            className={style.input}
            type="text"
            id="name"
            {...register("name", { required: true })}
          />
        </div>
        <div>
          <label className={style.label} htmlFor="language">
            language
          </label>
          <select
            className={style.select}
            type="text"
            id="language"
            {...register("language", { required: true })}
          >
            <option>Javascript</option>
            <option>Html</option>
            <option>CSS</option>
          </select>
        </div>
        <div>
          <label className={style.label} htmlFor="description">
            description
          </label>
          <textarea
            className={style.input}
            rows={7}
            type="text"
            id="description"
            placeholder="snippet description"
            {...register("description", { required: true })}
          />
        </div>
        <div>
          <label className={style.label} htmlFor="code">
            Code
          </label>
          <textarea
            className={style.input}
            rows={8}
            columns={8}
            type="text"
            id="code"
            {...register("code", { required: true })}
            placeholder="background: none;"
          />
        </div>
        <div>
          <button className={style.button}>Submit</button>
          <button className={style.button}>Cancel</button>
        </div>
      </form>
    </div>
  )
}
export default upload

Styling our Form

Above, we’ve created our form using the react-hook-form package. We’ve used the handleSubmit function useForm() in the ternary operator. Upon submission of the form, it determines if the submission made is to create or update an existing snippet. With register, we’ve added the required property to every field in our form. We’ve also added imports for a stylesheet named form.module.css where we have the following styles for our form:

.form {
  max-width: 800px;
  display: flex;
  justify-content: center;
  flex-direction: column;
  align-items: center;
}

.cont{
  background: rgb(48, 48, 255);
  height: 100%;
  min-height: 100vh;
  padding: 10px 0 0 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.select,
.input {
  display: block;
  box-sizing: border-box;
  width: 100%;
  border-radius: 4px;
  border: 1px solid black;
  padding: 10px 15px;
  margin-bottom: 15px;
  font-size: 14px;
}

.label{
  line-height: 2;
  text-align: left;
  display: block;
  margin: 5px;
  color: white;
  font-size: 14px;
  font-weight: 200;
}

.button {
  background : #fff;
  color: #444;
  border: none;
  border-radius: 5%;
  margin-right: 8px;
}

Creating an API endpoint to create snippets

To send the data from our form to the FaunaDB database, add the following code to the try...catch block in the createSnippet function in upload.js:

try {
  await fetch("/api/createSnippet", {
    method: "POST",
    body: JSON.stringify({ code, language, description, name }),
    headers: {
      "Content-type": "application/json"
    },
  })
  router.push("/")
} catch (error) {
  console.log(error)
}

Testing our Form

Run the code and navigate to the upload page. Now if we add a new snippet to the form and click on Submit, we’ll see what’s pictured below.

Snippet Upload page

When we navigate to our home component, we can see the created snippet.

Created a new snippet

Creating a Function to Edit Snippets

To create our edit snippet functionality, back in the Fauna.js file we’ll create and export a function to handle this task:

const updateSnippet = async (id, code, language, description, name) => {
  return await faunaClient.query(q.Update(q.Ref(q.Collection("codesnippet"), id), {
    data: {code, language, name, description},
  }))
}
module.exports = {
  ...
  updateSnippet,
}

Creating an API endpoint to edit snippets

This function is similar to the createSnippet function, but it also takes in a parameter of id. It uses this ID to identify which snippets are to be edited. If the id corresponds, we update the data with the other parameters. We’ll also create an endpoint file in the api directory called updateSnippet.js to handle the updates:

import { updateSnippet } from "../../Fauna"
export default async function handler(req, res) {
  const { id, code, language, description, name } = req.body
  if (req.method !== "PUT") {
    return res.status(405).json({ msg: "unauthorized" })
  }
  try {
    const updated = await updateSnippet(
      id,
      code,
      language,
      description,
      name
    )
    return res.status(200).json(updated)
  }
  catch (error) {
    console.log(error)
    res.status(500).json({ msg: "unauthorized" })
  }
}

Linking our Edit Button

Now, move over to the Snippets component and modify this component to make use of this function. First, we’ll import the Link module:

...
import Link from "next/link"

We also modify our edit button:

- <a>Edit</a>

+ <Link href={`/edit/${snippet.id}`}>
+   <a>Edit</a>
+ </Link>

Handling the edit snippet

When clicked, it sends a request to the page edit with the id of the selected snippet. In the pages folder, create a folder named edit with a file [id].js within it:

import { getSnippetById } from "../../Fauna"
import Upload from "../upload"
export default function Home({ snippet }) {
  const email = ""
  const user = ""
  return (
    <div>
      <h3>Update a snippet</h3>
      <Upload snippet={snippet} email={email} user={user}/>
    </div>
  )
}

export async function getServerSideProps(context) {
  try {
    //get and update record
    const id = context.params.id
  }
  catch (error) {
    console.log(error)
    context.res.statusCode = 302
    context.res.setHeader("Location", "/")
    return {props: {}}
  }
}

In [id].js, we’re passing the code snippet as props to the snippet upload page. However, this time the upload page will contain the data stored in the code snippet referenced by the id. To fetch the snippet by ID, we’ll need to create the getSnippetById function in the Fauna.js file:

const getSnippetById = async (id) => {
  const snippet = await faunaClient.query(q.Get(q.Ref(q.Collection("codesnippet"),id)))
  snippet.id = snippet.ref.id
  delete snippet.ref
  return snippet
}

module.exports = {
  getResponse,
  createSnippet,
  updateSnippet,
  getSnippetById,
}

As we export the function, back in the [id].js file, we can use it to fetch a particular snippet with its ID:

try {
  const id = context.params.id;
  const snippet = await getSnippetById(id);
  return {
    props: { snippet },
  };
} catch (error) {
  // as before
}

Modifying Stored Snippets

Now, in the upload.js file, we’ll modify it to be able to access the stored data if a snippet is to be edited:

- const { register, handleSubmit, errors, reset } = useForm()

+ const { register, handleSubmit, errors, reset } = useForm({
+   defaultValues: {
+     code: snippet ? snippet.data.code : "",
+     language: snippet ? snippet.data.language : "",
+     description: snippet ? snippet.data.description : "",
+     name: snippet ? snippet.data.name : "",
+   }
+ })

The code above checks if the snippet has stored data in it. If it returns true, it returns the data to the parameters: code, language, description and code. If it returns false, it returns an empty string.

Next, we’ll create a function to update the code snippet:

const createSnippet = async (data) => { ... }

const updateSnippet = async (data) => {
  const { code, language, description, name } = data
  const id = snippet.id
  try {
    await fetch("/api/updateSnippet", {
      method: "PUT",
      body: JSON.stringify({ code, language, description, name, id }),
      headers: {
        "Content-Type": "application/json",
      },
    })
    router.push("/")
  }
  catch (error) {
    console.log(error)
  }
}

return ( ,,, )

Testing Edit Snippet Functionality

If we run our code, we can edit the previously created code snippets by clicking on the Edit button, making changes to the data in the form and clicking on Submit.

Adding Delete Snippet Functionality

Now, if we return to the Home component in our browser, we’re able to edit and update code snippets. We can finally add the final functionality to delete our code snippet. Create and export a new function — deleteSnippet — in the Fauna.js file:

const deleteSnippet = async (id) => {
  return await faunaClient.query(q.Delete(q.Ref(q.Collection("codesnippet"),id)))
}

module.exports = {
  ...
  deleteSnippet,
}

Creating API endpoint for the delete functionality

We’ll create another endpoint for this function in our api folder called deleteSnippet.js and populate it with the following code:

import { deleteSnippet } from "../../Fauna"
export default async function handler(req, res) {
  if (req.method !== "DELETE") {
    return res.status(405).json({ msg: "unauthorized" })
  }
  const { id } = req.body
  try {
    const deleted = await deleteSnippet(id)
    return res.status(200).json(deleted)
  }
  catch (error) {
    console.log(error)
    res.status(500).join({ msg: "error occured" })
  }
}

Then, we modify the Snippets.js file to add the new functionality:

function Snippets({ snippet, snippetDeleted }) {
  ...
}

Then create a deleteSnippet function to fetch the endpoint from the api and delete the snippet referenced by the ID:

function Snippets({snippet, snippetDeleted}) {
  const deleteSnippet = async () => {
    try {
      await fetch("/api/deleteSnippet", {
        method: "DELETE",
        body: JSON.stringify({ id: snippet.id }),
        headers: {
          "Content-Type": "application/json",
        },
      });
      snippetDeleted();
    } catch (e) {
      console.log(e);
    }
  };

  return (
    <div className={styles.cont}>
      <p className={styles.lang}>{snippet.data.language}</p>
      <h3 className={styles.name}>{snippet.data.name}</h3>
      <p className={styles.descp}>{snippet.data.description}</p>

      <Code snippet={snippet}/>

      <div className={styles.links}>
        <Link href={`/edit/${snippet.id}`}>
          <a>Edit</a>
        </Link>
        <a onClick={deleteSnippet}>Delete</a>
      </div>
    </div>
  )
}

We’ve also updated the anchor element to call the deleteSnippet function when it’s clicked.

Testing Delete Functionality

We’ve added functionality to delete code snippets. We can now delete snippets by clicking on the Delete button in our app.

This concludes the functionalities for the snippet app. We’ll now proceed to add authentication measures to our app to enable only authorized users to create or modify snippets in our app.

User Authentication

Why do we need authentication? Currently, users can create snippets but they can also delete and modify snippets they didn’t create. We’ll need to provide a means to authorize users to access our site — and so the need for user authentication.

We’ll install next-auth for authentication via our CLI:

npm i next-auth

We’ll be using a JWT token for our authentication. JWT is a standard used to create access tokens for an application.

Create a folder named auth in your api folder and within it, create a file [...nextauth].js with the following code in it:

import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"

export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code",
    })
  ],
  jwt: {
    encryption: true
  },
  secret: process.env.secret,
  callbacks: {
    async jwt(token, account) {
      if (account ?.accessToken) {
        token.accessToken = account.accessToken
      }
      return token
    },
    redirect: async (url, _baseUrl)=>{
      if (url === "/profile") {
        return Promise.resolve("/")
      }
      return  Promise.resolve("/")
    }
  }
})

After this, we’ll wrap up our components in the _app.js file:

import '../styles/globals.css'
import {Provider} from "next-auth/client"

function MyApp({ Component, pageProps }) {
  return (
    <Provider session={pageProps.session}>
      <Component {...pageProps} />
    </Provider>
  )
}
export default MyApp

Handling Authentication

We’ll modify our Home component in index.js to return to our component if the user is authenticated, else it returns a button that leads to the authentication page:

import {signIn, signOut, useSession} from "next-auth/client"
...

Then within Home:

export default function Home() {
  const { data:snippets, mutate }=useSWR("api/snippets")

  const [session, loadingSession] = useSession()
  if (loadingSession) {
    <>
      <p>...authenticating</p>
    </>
  }
  ...
}

The code above checks if the app is loadingSession. If true, it returns the p tag block, else it returns the rest of our app if there is session. Next, we’ll render the “sign in” if there’s no session:

return (
  <div className={styles.container}>
    <Head> ... </Head>

    {!session && (
      <>
        <h1>Sign in to access snippet app</h1>
        <button onClick={() => signIn()}>Sign In</button>
      </>
    )}

    {session && (
      <>
        <main className={styles.main}>
          <h3>welcome {session.user.email}</h3>
          <button onClick={() => signOut()}>Sign Out</button>
          ...
        </main>
      </>
    )}
  </div>
)

To make use of the “Google login service”, we need access credentials from the Google cloud console. To get this, sign into your Google account and navigate to the Google Cloud console. Click on CREATE PROJECT on the page, enter the name of your project, and click on Create.

On the new page that opens, click on + CREATE CREDENTIALS in the top menu bar and finally select OAuth client ID in the dropdown menu.

Google Cloud console

On the page that opens, you’ll get a notification with a button asking you to “Configure Consent Screen”. Click on this button.

On the next page, Select External under the user type and click on Create. Enter the required fields for your “App name” and “Email” and click on Save and Continue.

In the Scopes and Test users sections, scroll down and click on Save and Continue.

Finally, click on Head back to dashboard and click on the Publish button.

Now, we can create our key by clicking on Credentials in the side menu, then Create Credentials in the top menu bar. Select Oauth Client ID in the dropdown and you’ll get a page requesting the application type.

Select Web Application then, under “Authorized JavaScript origins”, click Add URI and enter http://localhost. Finally, under “Authorized redirect URIs”, click Add URI and enter http://localhost/api/auth/callback/google in the field, before clicking on Create.

Oauth key creation

Copy the client ID and the client secret from the popup that opens and add them to the .env file:

GOOGLE_CLIENT_ID=id
GOOGLE_CLIENT_SECRET=secret

We can now log in using Google authentication to our app. Also, we’ll set up our upload.js file as a protected route so that unauthorized users can’t create new snippets:

import { getSession } from "next-auth/client"

function Upload({ snippet, user }) { ... }

export async function getServerSideProps(context) {
  const session = await getSession(context)
  if (!session) {
    context.res.writeHead(302, { Location: "/" })
    context.res.end()
    return {}
  }
  return {
    props: {
      user: session.user,
    }
  }
}

export default Upload;

Testing User Authentication

If we run our app with the npm run dev command, at first we’ll get a page requesting us to “sign in”. We can’t navigate to the upload page via the /upload path in our URL. We can only access our app when we use the Google login feature to sign in to our app.

Authorizing Users to Create a Snippet

Finally, we’ll modify the createSnippet functionality to add the user email to the database and then we’ll only show the Edit and Delete buttons if the email corresponds.

In Fauna.js, alter the createSnippet function like so:

const createSnippet = async (code, language, description, name, mail) => {
  return await faunaClient.query(q.Create(q.Collection("codesnippet"), {
    data:{code, language, description, name, mail}
  }))
}

In the createSnippet.js file, make the following changes:

- const { code, language, description, name } = req.body;
+ const { code, language, description, name, mail } = req.body;

- const createdSnippet = await createSnippet(code, language, description, name);
+ const createdSnippet = await createSnippet(code, language, description, name, mail);

In upload.js:

function upload({ snippet, user }) {
+  const email = user.email;
   ...
}

And alter the createSnippet function and the updateSnippet function, as follows:

const createSnippet = async (data) => {
  const { code, language, description, name, mail } = data;
  console.log(data)
  try {
    await fetch("/api/createSnippet", {
      method: "POST",
      body: JSON.stringify({ code, language, description, name, mail:email }),
      headers: {
        "Content-type": "application/json"
      },
    })
    router.push("/")
  } catch (error) {
    console.log(error)
  }
}

const updateSnippet = async (data) => {
  const { code, language, description, name } = data
  const id = snippet.id
  try {
    await fetch("/api/updateSnippet", {
      method: "PUT",
      body: JSON.stringify({ code, language, description, name, mail:email }),
      headers: {
        "Content-Type": "application/json",
      },
    })
    router.push("/")
  }
  catch (error) {
    console.log(error)
  }
}

We can now proceed with making the Edit and Delete buttons only display if the email corresponds.

First, we pass the user.mail as props to the Snippet component in index.js:

<Snippets
  key={snippet.id}
  snippet={snippet}
  snippetDeleted={mutate}
+ email={session.user.email}
/>

Then in Snippet.js:

function Snippets({ snippet, snippetDeleted, email }) {
...
  {email == snippet.data.mail && (
    <>
      <div className={styles.links}>
        <Link href={`/edit/${snippet.id}`}>
          <a>Edit</a>
        </Link>
        <a onClick={deleteSnippet}>Delete</a>
      </div>
    </>
  )}
  ...
}

Testing Our App

Run npm run dev in the CLI and open up the app in your browser. Now, if you create a new snippet, the user email is added to the database. If the email doesn’t correspond, the Edit and Delete buttons aren’t shown on the snippet display page. You can test this by logging in with a different email address than the one used to create the code snippets.

Conclusion

We’ve finally come to the end of this tutorial. We’ve learned how to build a CRUD app with Next.js and FaunaDB, and how to perform CRUD operations based on user authentication.

To check out the full code, visit the GitHub repository.

Frequently Asked Questions (FAQs) about Building a Code Snippet App with Next.js and FaunaDB

How does FaunaDB integrate with Next.js in building a code snippet app?

FaunaDB integrates seamlessly with Next.js in building a code snippet app. FaunaDB is a serverless cloud database that provides powerful capabilities for app development, such as native GraphQL support, ACID transactions, and multi-region global distribution. When building a code snippet app, you can use FaunaDB to store and manage your code snippets. Next.js, on the other hand, is a React framework that enables server-side rendering and generating static websites for React-based web applications. It can be used to build the frontend of your code snippet app, while FaunaDB handles the backend data management.

What are the benefits of using FaunaDB for my code snippet app?

FaunaDB offers several benefits for your code snippet app. Firstly, it provides a flexible and easy-to-use data model that supports relational, document, and graph models. This means you can store and manage your code snippets in a way that best suits your app’s needs. Secondly, FaunaDB’s serverless architecture means you don’t have to worry about server management, scaling, or capacity planning. This allows you to focus on building and improving your app. Lastly, FaunaDB’s strong consistency and ACID transactions ensure that your app’s data is always accurate and reliable.

How can I secure my code snippet app with FaunaDB?

FaunaDB provides robust security features that you can use to secure your code snippet app. It offers built-in identity and access management, allowing you to control who can access your app’s data and what they can do with it. You can also use FaunaDB’s ABAC (Attribute-Based Access Control) system to define fine-grained access policies based on user attributes. Additionally, FaunaDB supports secure client-side access to the database, so you can build secure, end-to-end encrypted applications.

Can I use other databases with Next.js for my code snippet app?

Yes, you can use other databases with Next.js for your code snippet app. Next.js is database-agnostic, meaning it can work with any database that you choose. However, the choice of database can significantly impact the performance, scalability, and ease of use of your app. Therefore, it’s important to choose a database that fits your app’s needs. FaunaDB is a great choice due to its serverless architecture, powerful capabilities, and ease of use.

How can I optimize the performance of my code snippet app with Next.js and FaunaDB?

There are several ways to optimize the performance of your code snippet app with Next.js and FaunaDB. Firstly, you can use Next.js’s server-side rendering and static generation features to improve the load times of your app. Secondly, you can use FaunaDB’s indexing and query optimization features to speed up data retrieval. Lastly, you can leverage FaunaDB’s multi-region global distribution to ensure that your app’s data is always close to your users, reducing latency.

Can I use FaunaDB with other frontend frameworks for my code snippet app?

Yes, you can use FaunaDB with other frontend frameworks for your code snippet app. FaunaDB provides a flexible and easy-to-use API that can be used with any frontend framework that supports HTTP requests. This includes popular frameworks like Angular, Vue.js, and Svelte. However, the integration process and capabilities may vary depending on the specific framework.

How can I handle errors in my code snippet app with Next.js and FaunaDB?

Both Next.js and FaunaDB provide robust error handling capabilities. In Next.js, you can use the built-in error page and error boundary features to catch and handle errors in your app. In FaunaDB, you can use the error handling features of the FQL (Fauna Query Language) to catch and handle database errors. Additionally, you can use third-party error tracking services to monitor and troubleshoot errors in your app.

Can I migrate my existing code snippet app to use Next.js and FaunaDB?

Yes, you can migrate your existing code snippet app to use Next.js and FaunaDB. The migration process involves rewriting your app’s frontend with Next.js and migrating your app’s data to FaunaDB. This can be a complex process depending on the size and complexity of your app, but it can bring significant benefits in terms of performance, scalability, and ease of use.

How can I test my code snippet app with Next.js and FaunaDB?

You can test your code snippet app with Next.js and FaunaDB using various testing tools and techniques. For the frontend, you can use Jest and React Testing Library to write unit and integration tests for your Next.js components. For the backend, you can use FaunaDB’s multi-tenant architecture to create separate databases for testing and production, ensuring that your tests don’t affect your live data.

How can I deploy my code snippet app with Next.js and FaunaDB?

You can deploy your code snippet app with Next.js and FaunaDB using various deployment platforms and services. For the frontend, you can use Vercel, the company behind Next.js, which provides a seamless deployment experience for Next.js apps. For the backend, you can use FaunaDB’s cloud service, which automatically handles deployment, scaling, and server management for you.

Victory TuduoVictory Tuduo
View Author

I am a web developer and technical writer. I build scalable web architectures and love authoring creative content.

FaunaDBNext.js
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week