DEV Community

loading...
Cover image for How To Build A Contact Manager Application With Next JS, Auth0 and Fauna

How To Build A Contact Manager Application With Next JS, Auth0 and Fauna

Babatunde Koiki
Software Developer | MERN | Pythonista 🐍 | JS lover | AI | Community lovers
・20 min read

Authored in connection with the Write with Fauna program.

Introduction

This article will demonstrate how to build a contact manager with Next.js and Fauna by walking you through the process of building a Google contact application clone.

What is NextJS?

Next.js is a React.js front-end framework with server-side capabilities, which makes it easy to build full-stack applications with

Some of its features and benefits include:

  1. Static Site Generation (SSG)
  2. Server-Side Rendering (SSR)
  3. Pre-rendering
  4. Better SEO
  5. Fast compilation times
  6. Automatic build size optimization

Prerequisites:

  1. Knowledge of React and JSX.
  2. Basic knowledge of Express.js
  3. Basic knowledge of Next.js
  4. npm and npx installed
  5. Installation of the create-next-app CLI tool

What you will learn in this article:

  1. Next.js app setup
  2. Routing on the client-side
  3. Routing on the server-side
  4. Authentication with Next.jsand Auth0
  5. Creating Fauna databases, collections, and indexes
  6. Building a fully functional app

Setting Up A Next.js Application

To set up a next.js app, all we need to do is to run the following command in the terminal:

npx create-next-app $relativePathToDir # npx create-next-app 
Enter fullscreen mode Exit fullscreen mode

This will create everything we need in the specified directory. You can look at the package.json file to check out the dependencies and scripts there.

Alt Text

As we can see, the package.json file has three scripts and three dependencies.

The dev command is used to start the app in development mode, while the build command is used to compile it. Meanwhile,the start command runs the app in production mode. Note, however, we need to compile our application before running it in production mode.

The app also has three dependencies: react, react-dom, and next itself.

Now, let's run our app. To do this, we need to type npm run dev in the application's root directory. We should see the following:

Alt Text

As we can see from the diagram above, there is are links to navigate from one site to another. We can also try to go to a random endpoint in the app. You should see the following, which is the default 404 page Next.js created for us:

Alt Text

Routing in NextJS

Unlike React.js, Next.js offers routing support out-of-the-box. In React.js, we need to install React Router dom to have routing abilities. However,with Next.js we do not need to do so. Rather, we just need to follow a particular syntax. Let's look at how we can handle both client-side and server-side routing in next js:

Client-Side Routing

In your pages folder, you can create a file, and that file name will be the route's endpoint. For example, say I want to have a /login endpoint; all I need to do is create a pages/login.js file. The page will then show a return value of the exported component.

Server-Side Routing

A folder called api should contain a file called hello.js with a simple express-like server in your pages folder. To test the API, go to the api/hello endpoint. You should see the following: {"name": "John Doe"}. That is the JSON object, which is sent as a response. Just as we route in the client, we create a file with the name we want to give the endpoint.

Complex Routes

Say we want to create a route like api/users/:userId, where userId is dynamic, create a route like api/users/contacts/follow, or api/users/:userId/follow/:friendId. How can we achieve this?.

Let's start with a route that is not dynamic – say api/users/contacts/follow or /users/contacts/follow. We need to chain it down using directories and sub-directories in our pages folder.

To create the /users/contacts/follow route, we need to create a pages/users/contacts/follow.js file in our application.

We can create a dynamic route, on the other hand, by naming the file with the path parameter enclosed in a square bracket. Say, for example, we want to create a route api/users/userId, we need to just create a file pages/api/users/[userId].js

To read more about routing in next.js, click here.

Authentication In Auth0 and NextJS

Handling authentication ourselves in some cases might not be a good idea because of security breaches. In this application, we'll be using Auth0 for authentication.

Let’s install the auth0js library for nextjs; in the terminal, we will have to type the following:

npm i @auth0/nextjs-auth0
Enter fullscreen mode Exit fullscreen mode

If you do not have an auth0 account, create one here. Head over to your dashboard and go to your applications page, then create a new application.

Alt Text

As we're using NextJS, we need to select regular web applications. After creating the application, we should redirect to its settings page. Scroll down and edit the application URL as shown below, then save your changes. You can check auth0 next.js documentation here.

Alt Text

Connecting Auth0 and NextJS

We need to get the following from our auth0 dashboard:

AUTH0_SECRET=#random character
AUTH0_BASE_URL=<http://localhost:3000> #base URL of the application
AUTH0_ISSUER_BASE_URL=#Your domain
AUTH0_CLIENT_ID=#Your client id
AUTH0_CLIENT_SECRET=#Your Client Secret
Enter fullscreen mode Exit fullscreen mode

To create environment variables in our next js app during development, we need to create a .env.local file in the root directory of our application. We need to create this file and pass in these values. Next js will parse the environment variables for us automatically, which we can use in the node environment of our app.

If we want to access these variables in the browser, we need to prefix the name with NEXT_PUBLIC_.

Now create a file called pages/api/auth/[...auth0].js, which will expose us to four different endpoints due to the fact that we’re destructuring the file: api/auth/login, api/auth/callback, api/auth/me and api/auth/logout that we can use in our application.

In the file you created, type the following:

import { handleAuth } from '@auth0/nextjs-auth0';

export default handleAuth();
Enter fullscreen mode Exit fullscreen mode

Also update your pages/_app.js file with the following:

import { UserProvider } from '@auth0/nextjs-auth0';
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
    return (
        <UserProvider>
            <Component {...pageProps} />
        </UserProvider>
    );
}

export default MyApp

Enter fullscreen mode Exit fullscreen mode

With these two things set up, we can have a login and logout button on our home page just to test the functionality of our app. Change the content of the pages/index.js file to the code snippet below:

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>Create Next App</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}>
                Welcome to <a href="/">Next.js!</a>
                </h1>
                <p className={styles.description}>
                    <a className={styles.code} href="/api/auth/login">Get started</a> by Creating an account or logging in
                </p>
                <p className={styles.description}>
                    <a className={styles.code} href="/api/auth/logout">Logout</a>
                </p>
                <p className={styles.description}>
                    <a className={styles.code} href="/api/auth/me">Profile</a>
                </p>
                <p className={styles.description}>
                    <a className={styles.code} href="/api/auth/callback">callback</a>
                </p>
            </main>
            <footer className={styles.footer}>
                <a
                href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
                target="_blank"
                rel="noopener noreferrer"
                >
                    Powered by{' '}
                    <span className={styles.logo}>
                        <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
                    </span>
                </a>
            </footer>
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

The app should now look like this; try to navigate to a different part of the app using the links. Start by creating an account or logging in; you should see the following page:

Alt Text
Alt Text

After signing in in, click on the profile link You should get a JSON response showing your profile data:

Alt Text

Navigate to the callback and logout route to see what happens.

Note that we won't be using the api/auth/me in the client-side of our app as auth0 provided us with a hook called useUser which returns the same thing when the user is logged in, and it returns null when the user is logged out.

Route Protection In Next JS and Auth0

It’s not enough to have an endpoint to log users in and out of the application; we need to be able to protect unauthenticated users from viewing some pages in the application and also restrict access to some APIs. Auth0 provides us with two functions that help ensure only authenticated users have access to a particular resource: withApiAuthRequired and withPageAuthRequired

These functions take in a callback function, while we use withApiAuthRequired in the API part of the app, and we use withPageAuthRequired in the components.

Let's now look at how we can restrict unauthenticated users to get a resource from the endpoint api/user and the dashboard page.

We'll need to create the following files: pages/api/user.js and pages/dashboard.js We need to put the following in the pages/api/user.js file:

import { withApiAuthRequired ,getSession } from "@auth0/nextjs-auth0"

export default withApiAuthRequired(async (req, res) => {
    const user = getSession(req, res).user // the getSession function is used to get the session object that's created in the app. Which is where auth data is kepy
        res.json({user})
    })

Enter fullscreen mode Exit fullscreen mode

In our pages/dashboard.js file, let’s type the following:

import { withPageAuthRequired, useUser } from '@auth0/nextjs-auth0'

const Dashboard = () => {
    const {user} = useUser()
    return (
        <div>
            <main>
                {user && (
                    <div>
                        <div>
                            {user.email} {!user.email_verified && <span>Your account is not verified</span>}
                        </div>
                    </div>
                )}
            </main>
        </div>
    )
}

export const getServerSideProps = withPageAuthRequired()

export default Dashboard

Enter fullscreen mode Exit fullscreen mode

If you go to the dashboard endpoint without logging in, it redirects to the login page. Similarly, if you go to the api/user endpoint, it will return with an error message. We've successfully protected routes both on the client and server-side.

Connecting Our Application To Fauna

Alt Text

Creating A Fauna Database

To create a Fauna database, head to the dashboard.

Alt Text

Next, click on the New Database button, enter the database name, and click enter.

Creating Fauna Collections

A collection is a group of documents(rows) with the same or a similar purpose. A collection acts similarly to a table in a traditional SQL database.

In the app we’re creating, we'll have one collection contacts. The user collection is where we’ll be storing our contact data.

To create these, click on the database you created and New Collection. Enter only the collection name (contacts), then click save.

Alt Text

Creating Fauna Indexes

Use indexes to quickly find data without searching each document in a database collection every time you need to access one. Indexes using one or more fields of a database collection. To create a Fauna index, click on the indexes section on the left of your dashboard.

Alt Text

In this application, we will be creating the one index which is the user_contacts index, this is used to retrieve all passwords created by a particular user.

Generating Your Fauna Secret

The Fauna secret key connects an application or script to the database, and it is unique per database. To generate it, go to your dashboard’s security section and click on New Key. Enter your key name, and a new key will be generated for you. Paste the key in your .env.local file in this format: REACT_APP_FAUNA_KEY={{ API key }}

Building Our Application

First, we need to figure out the structure of our application. Our application will have the following endpoints:

  1. /: home route
  2. /dashboard: The dashboard route. Only authenticated users can access this page.
  3. api/contacts: This is an API. It will support the GET HTTP method for getting all the contacts created by the user and the POST HTTP method for creating a new contact
  4. api/contacts/:contactId: This is also an API which will support GET, PUT and theDELETE HTTP method for getting a single contact, updating it, and deleting a contact respectively.

Now we know the routes that we need to create and automatically we know the files we need to create to achieve this, we also need to have some components that will be used in the app. Thus, we will create a components folder in the root directory of our app and put each component there:

  1. Navbar: This is the navbar of the app,. We Will create a file called components/Navbar.js for this.
  2. Contact: This contains details of a single contact detail. We won't have a separate file for this.
  3. Contacts: This will use the Contact component and display all the contacts created by the authenticated user. We will create a file called components/Contacts and put both the Contacts and Contact components there.
  4. BaseModal: is the component we will build all our modals upon. We will place it in a file called components/BaseModal.js.
  5. CreateContact.modal: is the component that creates a modal for creating a new contact. We will place it in a file called CreateContact.modal.js.
  6. EditContact.modal: This is the component that creates a modal for editing a contact. We will add it to a file called EditContact.modal.js

We also need to have a file that handles the logic of database modeling, so we won’t have to be writing queries directly in the api folder. This file models.js will be in the root directory of our app.

We also need to install the remaining dependencies. Type the following in the root directory of your application:

npm i faunadb axios @fortawesome/react-fontawesome @fortawesome/free-solid-svg-icons @fortawesome/fontawesome-svg-core @fortawesome/fontawesome-free react-bootstrap
Enter fullscreen mode Exit fullscreen mode

Models

In your models.js, type the following

import faunadb, {query as q} from 'faunadb'

const client = new faunadb.Client({secret: process.env.REACT_APP_FAUNA_KEY})

export const createContact = async (
    firstName, 
    lastName, 
    email, 
    phone,
    user, 
    jobTitle, 
    company,
    address,
    avatar
    ) => {
    const date = new Date()
    const months = [
    "January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December"
    ]
    let newContact = await client.query(
        q.Create(
            q.Collection('contacts'),
            {
                data: {
                    firstName,
                    lastName,
                    email,
                    phone,
                    company,
                    jobTitle,
                    address,
                    avatar,
                    created__at: `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`,
                    user: {
                        id: user.sub
                    }
                }
            }
        )
    )
    if (newContact.name === 'BadRequest') return
    newContact.data.id = newContact.ref.value.id
    return newContact.data
}

export const getContactsByUserID = async id => {
    try {
        let userContacts = await client.query(
            q.Paginate(
                q.Match(q.Index('user_contacts'), id)
            )
        )
        if (userContacts.name === "NotFound") return
        if (userContacts.name === "BadRequest") return "Something went wrong"
        let contacts = []
        for (let contactId of userContacts.data) {
            let contact = await getContact(contactId.value.id)
            contacts.push(contact)
        }
        return contacts
    } catch (error) {
        if (error.message === 'instance not found') return []
        return
    }
}

export const getContact = async id => {
    let contact = await client.query(
        q.Get(q.Ref(q.Collection('contacts'), id))
    )
    if (contact.name === "NotFound") return
    if (contact.name === "BadRequest") return "Something went wrong"
    contact.data.id = contact.ref.value.id
    return contact.data
}

export const updateContact = async (payload, id) => {
    let contact = await client.query(
    q.Update(
        q.Ref(q.Collection('contacts'), id),
            {data: payload}
        )
    )
    if (contact.name === "NotFound") return
    if (contact.name === "BadRequest") return "Something went wrong"
    contact.data.id = contact.ref.value.id
    return contact.data
}

export const deleteContact = async id => {
    let contact = await client.query(
        q.Delete(
            q.Ref(q.Collection('contacts'), id)
        )
    )
    if (contact.name === "NotFound") return
    if (contact.name === "BadRequest") return "Something went wrong"
    contact.data.id = contact.ref.value.id
    return contact.data
}

Enter fullscreen mode Exit fullscreen mode

The logic of this file is pretty straightforward. We have functions to create a new contact, get all contacts created by a user, obtain a single contact, update a single contact and delete a single contact. You might be wondering why we do not handle the user dB; well we do not need to in this case because we do not have a complex dB. We just need to be able to figure out the owner of a particular contact, and auth0 gives us access to the ID and email of the logged-in user, amongst other things.

Components

Navbar Component

In your components/Navbar.js file, type the following:

import {
    Navbar, Nav
} from 'react-bootstrap'
import { useUser } from '@auth0/nextjs-auth0';
import Image from 'next/image';

const NavbarComponent = () => {
    const {user, isLoading, error} = useUser()
    return (
        <Navbar fixed="top" collapseOnSelect expand="lg" bg="dark" variant="dark">
            <Navbar.Brand className="mx-2 mx-md-4" href="/">Contact Manager</Navbar.Brand>
            <Navbar.Toggle aria-controls="responsive-navbar-nav" />
            <Navbar.Collapse className="d-lg-flex justify-content-end" id="responsive-navbar-nav">
                {(!user & !error) ? 
                    <>
                        <Nav.Link className="text-light" href="api/auth/login">Sign In </Nav.Link> : 
                        <Image alt="avatar" loader={myLoader} src={`https://ui-avatars.com/api/?background=random&name=John+Doe`} width="35" height="35" className="rounded-circle" />
                    </> :
                    <>
                        <Nav.Link className="text-light" href="/dashboard">Dashboard</Nav.Link>
                        <Nav.Link className="text-light" href="api/auth/logout">Sign Out</Nav.Link>
                        <Nav.Link href="/profile">
                            <Image alt="avatar" loader={myLoader} src={user.picture || `https://ui-avatars.com/api/?background=random&name=${firstName}+${lastName}`} width="35" height="35" className="rounded-circle" />
                        </Nav.Link>
                    </>
                }
            </Navbar.Collapse>
        </Navbar>
    )
}

const myLoader=({src})=>{
    return src;
}

export default NavbarComponent

Enter fullscreen mode Exit fullscreen mode

We used the useUser hook here to determine if the user is logged in or not since we want to return things from this component dynamically. We also have a myLoader function at the bottom of the file, and this is because we are using the Image tag with a link.

BaseModal Component

In your components/BaseModal.js file, type the following:

import Modal from 'react-bootstrap/Modal'
import Container from "react-bootstrap/Container";
import Button from "react-bootstrap/Button";
import Form from "react-bootstrap/Form";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";

const BaseModal = (props) => {
    const onHide = () => {
        if (props.create) {
            props.updateFirstName('')
            props.updateLastName('')
            props.updateEmail('')
            props.updatePhone('' )
            props.updateAddress('')
        }
        props.onHide()
    }
    return (
        <Modal
        {...props}
        size="xlg"
        aria-labelledby="contained-modal-title-vcenter"
        centered
        onHide={onHide}
        >
            <Modal.Header closeButton>
                <Modal.Title id="contained-modal-title-vcenter">
                    {props.header && props.header}
                    {props.title && props.title}
                </Modal.Title>
            </Modal.Header>
            <Modal.Body className="show-grid">
                <Container>
                    <Form>
                        <Row>
                            <Form.Group as={Col} className='form-group'>
                                <Form.Control placeholder="First name" className='form-control' value={props.firstName} onChange={e => {props.updateFirstName(e.target.value)}}/>
                            </Form.Group>
                            <Form.Group as={Col} className='form-group'>
                                <Form.Control placeholder="Last name" className='form-control' value={props.lastName} onChange={e => {props.updateLastName(e.target.value)}}/>
                            </Form.Group>
                        </Row>
                        <Row>
                            <Form.Group as={Col}>
                                <Form.Control type="email" placeholder="Email" value={props.email} onChange={e => {props.updateEmail(e.target.value)}}/>
                            </Form.Group>
                        </Row>
                        <Row>
                            <Form.Group as={Col}>
                                <Form.Control type="phone" placeholder="Phone number(+2348180854296)" value={props.phone} onChange={e => {props.updatePhone(e.target.value)}}/>
                            </Form.Group>
                        </Row>
                        <Row>
                            <Form.Group as={Col}>
                                <Form.Control placeholder="Address" value={props.address} onChange={e => {props.updateAddress(e.target.value)}}/>
                            </Form.Group>
                        </Row>
                    </Form>
                </Container>
            </Modal.Body>
            <Modal.Footer>
                <Button variant="danger" onClick={onHide}>Close</Button>
                <Button variant="success" onClick={props.create ? props.handleCreate: props.handleEdit} disabled={(!props.firstName || !props.lastName || !props.phone) ? true : false}>{props.btnText}</Button>
            </Modal.Footer>
        </Modal>
    );
}

export default BaseModal

Enter fullscreen mode Exit fullscreen mode

Contacts and Contact component

In your components/Contacts.js file, type the following:

import Image from 'next/image';
import Button from 'react-bootstrap/Button'
import Table from 'react-bootstrap/Table'
import { useState } from 'react'
import EditContactModal from './EditContact.modal'

const Contact = ({
    id,
    firstName,
    lastName,
    email,
    phone,
    address
    avatar,
    handleDelete,
    handleEdit
    }) => {
    const [editModal, setEditModal] = useState(false)
    const editContact = () => {
        setEditModal(true)
    }

    const deleteContact = () => {
        handleDelete(id)
        alert('Contact deleted successfully')
    }

    return (
        <tr>
            <td>
                <Image alt="avt" loader={myLoader} src={avatar} width="35" height="35" className="rounded-circle" />
            </td>
            <td>{firstName} {lastName}</td>
            <td>
                <a href={`mailto:${email}`}>{email}</a>
            </td>
            <td>
                <a href={`tel:${phone}`}>{phone}</a>
            </td>
            <td>{address}</td>
            <td><Button onClick={editContact}>Edit</Button></td>
            <td><Button onClick={deleteContact}>Delete</Button></td>

            <EditContactModal
            show={editModal}
            firstname={firstName}
            lastname={lastName}
            email={email}
            phone={phone}
            address={address}
            title={"Edit Contact for "+firstName}
            onHide={() => {
                let n = window.confirm("Your changes won't be saved...")
                if (n) setEditModal(false)
            }}
            onEdit ={(contact) => {
                contact.id = id
                handleEdit(contact)
                alert(`Contact for ${firstName} updated successfully`)
                setEditModal(false)
            }}
            />
        </tr>
    )
}

const Contacts = ({contacts, handleEdit, handleDelete}) => {
    return (
    <>
        {!contacts && 'Fetching contacts...'}
        <Table striped bordered hover responsive>
            <thead>
                <tr>
                <th>avatar</th>
                <th>Name</th>
                <th>Email</th>
                <th>Phone</th>
                <th>Address</th>
                <th>Edit</th>
                <th>Delete</th>
                </tr>
            </thead>
            <tbody>
                {contacts.map(ele => <Contact {...ele} 
                    key={ele.id} 
                    handleEdit={handleEdit} 
                    handleDelete={handleDelete} />)} 
            </tbody>
        </Table>
    </>
    )
}

const myLoader=({src})=>{
    return src;
}

export default Contacts

Enter fullscreen mode Exit fullscreen mode

Create Contact Modal

In your CreateContact.modal.js file, type the following:

import BaseModal from './BaseModal'
import { useState } from 'react'

const CreateContactModal = (props) => {
    const [firstName, setFirstName] = useState('')
    const [lastName, setLastName] = useState('') 
    const [email, setEmail] = useState('')
    const [phone, setPhone] = useState('') 
    const [address, setAddress] = useState('')

    const handleCreate = () => {
        const payload = {
            firstName,
            lastName,
            email,
            phone, 
            address
        }
        props.onCreate(payload)
    }

    return <BaseModal
        show={props.show}
        onHide={props.onHide}
        firstName={firstName}
        lastName={lastName}
        email={email}
        phone={phone}
        address={address}
        updateFirstName={newInput => setFirstName(newInput)}
        updateLastName={newInput => setLastName(newInput)}
        updateEmail={newInput => setEmail(newInput)}
        updatePhone={newInput => setPhone(newInput)}
        updateAddress={newInput => setAddress(newInput)}
        header="Create New Contact"
        btnText="Create"
        handleCreate={handleCreate}
        create={true}
    />
}

export default CreateContactModal

Enter fullscreen mode Exit fullscreen mode

This component uses the BaseModal.js file and passes props to the component.

Edit Contact Modal

In your components/EditContact.modal.js file, type the following:

import BaseModal from './BaseModal'
import { useState } from 'react'

const EditContactModal = props => {
    const [firstName, setFirstName] = useState(props.firstname)
    const [lastName, setLastName] = useState(props.lastname) 
    const [email, setEmail] = useState(props.email)
    const [phone, setPhone] = useState(props.phone) 
    const [address, setAddress] = useState(props.address)

    const onEdit = () => {
        const payload = {
            firstName
            lastName,
            email,
            phone, 
            address
        }
        props.onEdit(payload)
    }

    return <BaseModal
        show={props.show}
        onHide={props.onHide}
        title={props.title}
        firstName={firstName}
        lastName={lastName}
        email={email}
        phone={phone}
        address={address}
        updateFirstName={newInput => setFirstName(newInput)}
        updateLastName={newInput => setLastName(newInput)}
        updateEmail={newInput => setEmail(newInput)}
        updatePhone={newInput => setPhone(newInput)}
        updateAddress={newInput => setAddress(newInput)}
        btnText="Edit"
        handleEdit={onEdit}
        create={false}
    />
}

export default EditContactModal

Enter fullscreen mode Exit fullscreen mode

You might notice that the pages,/index.js file has a Meta tag.All pages should have their meta tag for SEO optimization.

Let’s create a components/MetaData.js file:

MetaData Component

In your components/MetaData.js file, type the following:

import Head from 'next/head'

const MetaData = ({title}) => {
    return (
        <Head>
            <title>{`Contact Manager App ${title && "| " +title}`}</title>
            <meta name="description" content="A simple Contact Manager" />
            <link rel="icon" href="/favicon.ico" />
        </Head>
    )
}

export default MetaData

Enter fullscreen mode Exit fullscreen mode

API

Before we start creating our screens, it’s ideal for our backend to be complete since we will consume the APIs in the frontend of our app.

We need the following files for our API, excluding the auth endpoint:

  1. api/contacts - we need to create a pages/api/contacts.js file

    a. GET - get all contacts.
    b. POST - create a new contact.

  2. api/contacts/:id - we need to create a pages/api/contacts/[id].js file

    a. GET - get a single contact
    b. PUT - update a single contact
    c. DELETE - delete a single contacts

Create and Get all contacts

In your pages/api/contacts.js file, type the following:

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { withApiAuthRequired, getSession } from "@auth0/nextjs-auth0"
import { createContact, deleteContact, getContactsByUserID } from "../../models"

export default withApiAuthRequired(async (req, res) => {
    const user = getSession(req, res).user
    if (req.method === 'POST') {
        let {
            firstName, lastName, email,
            company, jobTitle, phone, address, avatar
        } = req.body
        let newContact = await createContact(
            firstName, lastName, 
            email, phone,
            user, jobTitle, 
            company, address, avatar
        )
        res.status(201).json({ 
            message: "Successfully created contact",
            data: newContact,
            status: 'ok'
    })
    } else if (req.method === 'GET') {
        let contacts = await getContactsByUserID(user.sub)
        if (!contacts) return res.status(400).json({
            message: 'Something went wrong',
            data: null,
            status: false
        })
        res.status(200).json({ 
            message: "Successfully retrieved contacts",
            data: contacts,
            status: 'ok'
        })
    } else {
        res.status(405).json({
            message: 'Method not allowed',
            data: null,
            status: false
        })
    }
})

Enter fullscreen mode Exit fullscreen mode

In this file, we used the getSession function to get the current user from the request and response object. We then used this to set the contact creator and get contacts created by the user.

UPDATE, DELETE and GET a single Contact

In your pages/api/contacts/[id].js type the following:

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { withApiAuthRequired ,getSession } from "@auth0/nextjs-auth0"
import { deleteContact, getContact, updateContact } from "../../../models"

export default withApiAuthRequired(async (req, res) => {
    const user = getSession(req, res).user
    if (req.method === 'PUT') {
        let contact = await updateContact(
            req.body, req.query.id
        )
        res.status(201).json({ 
            message: "Successfully updated contact",
            data: contact,
            status: 'ok'
        })
    } else if (req.method === 'GET') {
        let contact = await getContact(req.query.id)
        res.status(200).json({ 
            message: "Successfully retrieved contact",
            data: contact,
            status: 'ok'
        })
    } else if (req.method === 'DELETE') {
        let contact = await getContact(req.query.id)
        if (contact.user.id !== user.sub) {
            return res.status(403).json({
                message: "Forbidden",
                status: false,
                data: null
            })
        }
        contact = await deleteContact(req.query.id)
        res.status(200).json({ 
            message: "Successfully deleted contact",
            data: contact,
            status: 'ok'
        })
    } else {
        res.status(405).json({
            message: 'Method not allowed',
            data: null,
            status: false
        })
    }
})

Enter fullscreen mode Exit fullscreen mode

With this we have our API all set up. You can test it by going to different endpoints using an API testing tool, like Postman.

Pages

Now we finished creating our components and APIs,, we need to create the pages and use the above.

Index Page

Change the content of your pages/index.js file to the following:

import Image from 'next/image';
import { useUser } from '@auth0/nextjs-auth0';
import MetaData from '../components/MetaData'
import styles from '../styles/Home.module.css'

export default function Home() {
    const { error, isLoading } = useUser();
    if (isLoading) return <div>Loading...</div>;
    if (error) return <div>{error.message}</div>;

    return (
        <div>
        <MetaData title="" />
        <main className={styles.container}>
            <Image className={styles.img} alt="home" src="/home.jpeg" width="400" height="200" />
        </main>
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

This page just returns an image as the content of the app. You might be wondering: where will our navbar be? So as not to call the navbar more than once, we’ll place it in our pages/_app.js file. Basically, this file is what is served, and it changes based on what is happening on the current page.

Dashboard Page

In your pages/dasboard.js file, type the following:

import { useEffect, useState } from 'react'
import { withPageAuthRequired, useUser } from '@auth0/nextjs-auth0'
import Button from 'react-bootstrap/Button'
import axios from 'axios'
import CreateContactModal from '../components/CreateContact.modal'
import Contacts from '../components/Contacts'
import MetaData from '../components/MetaData'
import styles from '../styles/Home.module.css'

const Dashboard = () => {
    const {user} = useUser()
    const [contacts, setContacts] = useState([])
    const [createModalShow, setCreateModalShow] = useState(false);

    const handleHide = () => {
        let n = window.confirm("Your changes won't be saved...")
        if (n) setCreateModalShow(false)
    }

    useEffect(async () => {
        let res = (await axios.get(`/api/contacts`)).data
        res = await res.data
        setContacts(res.reverse())
    }, [])

    const createContact = async payload => {
        payload.avatar = `https://ui-avatars.com/api/?background=random&name=${payload.firstName}+${payload.lastName}`
        let newContact = (await axios.post(`/api/contacts`, payload)).data
        setContacts([newContact.data, ...contacts])
    }

    const editContact = async payload => {
        let id = payload.id
        delete payload.id
        let replacedContact = (await axios.put(`/api/contacts/${id}`, payload)).data
        setContacts(contacts.map(contact => contact.id === id? replacedContact.data : contact))
    }

    const deleteContact = async id => {
        (await axios.delete(`/api/contacts/${id}`)).data
        setContacts(contacts.filter(contact => contact.id !== id))
    }

    return (
        <div>
            <MetaData title="Dashboard" />
            <main>
                {user && (
                    <div className={styles.dashboardContainer}>
                        <div>
                            <img alt="avatar" src={user.picture} className="rounded-circle m-3" width="100" height="100"/> 
                            <span>Welcome {user.nickname.toLowerCase().charAt(0).toUpperCase()+user.nickname.toLowerCase().slice(1)}</span> 
                            {!user.email_verified && <div>Your account is not verified</div>}
                    </div>
                    <div>
                        <Button variant="primary" onClick={() => setCreateModalShow(true)}>
                            Create New Contact
                        </Button>
                        <CreateContactModal
                            show={createModalShow}
                            onHide={handleHide}
                            onCreate ={(payload) => {createContact(payload); setCreateModalShow(false)}}
                        />
                    </div>
                </div>
                )}
            </main>
            <Contacts 
                contacts={contacts}
                handleEdit={(id) => editContact(id)}
                handleDelete={(id) => deleteContact(id)} 
            />
        </div>
    )
}

export const getServerSideProps = withPageAuthRequired()

export default Dashboard

Enter fullscreen mode Exit fullscreen mode

What is happening here is pretty straightforward: We are getting contacts the user created when the page loads, and we render it. We also show some details about the logged-in user, and we have a create contact button.

Before we can run our application, we need to make one change: we need to add the navbar to the pages/_app.js file.

Root Component

Update the content of your pages/_app.js file with the following:

import React, { useEffect, useState } from 'react'
import { UserProvider } from '@auth0/nextjs-auth0';
import axios from 'axios'
import MetaData from '../components/MetaData'
import NavbarComponent from '../components/Navbar'
import 'bootstrap/dist/css/bootstrap.min.css';
import '../styles/globals.css'

export default function App({ Component, pageProps }) {
    return (
        <UserProvider>
            <NavbarComponent />
            <Component {...pageProps} />
        </UserProvider>
    );
}

Enter fullscreen mode Exit fullscreen mode

Running Our Application

We have successfully built our application. Next, we need to run it in development mode. If you run your application, you should see the following:

Alt Text

After signing in, you should get redirected to the home page after signing in with the navbar being different.

Alt Text

Go to the dashboard endpoint and create some contacts. Also, edit some of them and watch how the dashboard component is changing. You can also check the network request. You’ll notice that our Fauna secret key isn’t present as we handle this from the server.

Alt Text

Alt Text

Alt Text

Alt Text

We’ve successfully tested our application.

Next Steps

We have now built our application, but we're never really done developing it, as there's always room for improvement.. These are some of the things we can add to this application to make it look better:

  1. We can improve the appearance of the UI
  2. We can add a PWA feature to our application
  3. We can also create a profile page for the logged-in user, where they can update their profile.

Conclusion

This article has offered a deep dive into Next.js and why we should use it in our projects. It also explains how to build a fully functional application with authentication features using NextJS and auth0 for authentication, and Fauna as our database provider.

Do you have something to add to this project? kindly let me know. You can reach out to me via Twitter. If you like this project, kindly give it a star on GitHub. You can also check out the deployed app here.

Discussion (0)