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:
- Static Site Generation (SSG)
- Server-Side Rendering (SSR)
- Pre-rendering
- Better SEO
- Fast compilation times
- Automatic build size optimization
Prerequisites:
- Knowledge of React and JSX.
- Basic knowledge of Express.js
- Basic knowledge of Next.js
- npm and npx installed
- Installation of the create-next-app CLI tool
What you will learn in this article:
- Next.js app setup
- Routing on the client-side
- Routing on the server-side
- Authentication with Next.jsand Auth0
- Creating Fauna databases, collections, and indexes
- 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
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.
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:
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:
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
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.
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.
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
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();
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
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>
)
}
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:
After signing in in, click on the profile link You should get a JSON response showing your profile data:
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})
})
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
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
Creating A Fauna Database
To create a Fauna database, head to the dashboard.
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.
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.
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:
-
/
: home route -
/dashboard
: The dashboard route. Only authenticated users can access this page. -
api/contacts
: This is an API. It will support theGET
HTTP method for getting all the contacts created by the user and thePOST
HTTP method for creating a new contact -
api/contacts/:contactId
: This is also an API which will supportGET
,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:
-
Navbar
: This is the navbar of the app,. We Will create a file calledcomponents/Navbar.js
for this. -
Contact
: This contains details of a single contact detail. We won't have a separate file for this. -
Contacts
: This will use theContact
component and display all the contacts created by the authenticated user. We will create a file calledcomponents/Contacts
and put both theContacts
andContact
components there. -
BaseModal
: is the component we will build all our modals upon. We will place it in a file calledcomponents/BaseModal.js
. -
CreateContact.modal
: is the component that creates a modal for creating a new contact. We will place it in a file calledCreateContact.modal.js
. -
EditContact.modal
: This is the component that creates a modal for editing a contact. We will add it to a file calledEditContact.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
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
}
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
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
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
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
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
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
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:
-
api/contacts
- we need to create apages/api/contacts.js
filea. GET - get all contacts.
b. POST - create a new contact. -
api/contacts/:id
- we need to create apages/api/contacts/[id].js
filea. 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
})
}
})
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
})
}
})
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>
)
}
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
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>
);
}
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:
After signing in, you should get redirected to the home page after signing in with the navbar being different.
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.
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:
- We can improve the appearance of the UI
- We can add a PWA feature to our application
- 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.
Top comments (0)