Quickstart: Next.js
Introduction
This quickstart provides the steps you need to build a Next.js app powered by Nhost for the backend. It includes:
- Database: PostgreSQL
- Instant GraphQL API: Hasura
- Authentication: Hasura Auth
- Storage: Hasura Storage
By the end of this guide, you'll have a full-stack app that allows users to sign in to access a protected dashboard and update their profile information.
Prerequisites
Before getting started, let's make sure that your development environment is ready.
You'll need Node.js version 12 or later: install it from here.
Project setup
Create a new Nhost app
First things first, we need to create a new Nhost project.
So, log in to your Nhost dashboard and click the Create Your First Project button.
Next, give your new Nhost project a name, select a geographic region for your Nhost services and click Create Project.
After a few seconds, you should get a PostgreSQL database, a GraphQL API with Hasura, file storage, and authentication set up.
You can also connect your Nhost project to a Git repository at GitHub. When you do this, any updates you push to your code will automatically be deployed. Learn more
Initialize the app
Create a Next.js app
The simplest way to create a new Next.js application is by using the tool called
create-next-app
, which bootstraps a Next.js app for you without the hassle of
configuring everything yourself.
So, open your terminal, and run the following command:
npx create-next-app my-nhost-app --example "https://github.com/nhost/quickstart-nextjs"
This command uses an existing template, through the --example
flag, which already contains the React components and pages we'll use for this guide.
You can now cd
into your project directory:
cd my-nhost-app
And run the development server with the following command:
- npm
- Yarn
npm run dev
yarn dev
If everything is working fine, your Next.js development server should be running on port 3000. Open http://localhost:3000 from your browser to check this out.
Configure Nhost with Next.js
To work with Nhost from within our Next.js app, we'll use the Next.js SDK provided by Nhost. It's a wrapper around the Nhost React SDK which gives us a way to interact with our Nhost backend using React hooks.
You can install the Nhost Next.js SDK with:
- npm
- Yarn
npm install @nhost/nextjs graphql
yarn add @nhost/nextjs graphql
Next, open your _app.js
file as we'll now configure Nhost inside our app.
The Nhost Next.js SDK comes with a React provider named NhostProvider
that
makes the authentication state and all the provided React hooks available in our
application.
Use the following code to instantiate a new Nhost client and link it to your Nhost backend:
import { UserProvider } from '../UserProvider';
// highlight-start
import { NhostProvider, NhostClient } from '@nhost/nextjs';
// highlight-end
// highlight-start
const nhost = new NhostClient({
subdomain: process.env.NEXT_PUBLIC_NHOST_SUBDOMAIN || '',
region: process.env.NEXT_PUBLIC_NHOST_REGION || ''
});
// highlight-end
function MyApp({ Component, pageProps }) {
return (
{/* highlight-next-line */}
<NhostProvider nhost={nhost} initial={pageProps.nhostSession}>
<UserProvider>
{/* ... */}
</UserProvider>
{/* highlight-next-line */}
</NhostProvider>
);
}
Finally, store the environment variables for subdomain
and region
in .env.development
:
NEXT_PUBLIC_NHOST_SUBDOMAIN=[subdomain]
NEXT_PUBLIC_NHOST_REGION=[region]
You find your Nhost project's subdomain
and region
in the project overview:
Don't forget to restart your Next.js server after saving your .env.development
file to load your new environment variable.
Do you use the Nhost CLI? Learn how to set subdomain
and region
in the CLI documentation.
Build the app
Add authentication
1. Sign-up
The next step is to allow our users to authenticate into our application. Let's start with implementing the sign-up process.
For that, we'll use the useSignUpEmailPassword
hook provided by the Nhost
Next.js SDK within our SignUp
component.
So, open up the corresponding file from your project, and use the following code:
import styles from '../styles/components/SignUp.module.css'
import { useState } from 'react'
import { useRouter } from 'next/router'
import { useSignUpEmailPassword } from '@nhost/nextjs'
import Link from 'next/link'
import Image from 'next/image'
import Input from './Input'
import Spinner from './Spinner'
const SignUp = () => {
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const router = useRouter()
const { signUpEmailPassword, isLoading, isSuccess, needsEmailVerification, isError, error } =
useSignUpEmailPassword()
const handleOnSubmit = async (e) => {
e.preventDefault()
await signUpEmailPassword(email, password, {
displayName: `${firstName} ${lastName}`.trim(),
metadata: {
firstName,
lastName
}
})
}
if (isSuccess) {
router.push('/')
return null
}
const disableForm = isLoading || needsEmailVerification
return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles['logo-wrapper']}>
<Image src="/logo.svg" alt="logo" layout="fill" objectFit="contain" />
</div>
{needsEmailVerification ? (
<p className={styles['verification-text']}>
Please check your mailbox and follow the verification link to verify your email.
</p>
) : (
<form onSubmit={handleOnSubmit} className={styles.form}>
<div className={styles['input-group']}>
<Input
label="First name"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
disabled={disableForm}
required
/>
<Input
label="Last name"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
disabled={disableForm}
required
/>
</div>
<Input
type="email"
label="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={disableForm}
required
/>
<Input
type="password"
label="Create password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={disableForm}
required
/>
<button type="submit" disabled={disableForm} className={styles.button}>
{isLoading ? <Spinner size="sm" /> : 'Create account'}
</button>
{isError ? <p className={styles['error-text']}>{error?.message}</p> : null}
</form>
)}
</div>
<p className={styles.text}>
Already have an account?{' '}
<Link href="/sign-in">
<a className={styles.link}>Sign in</a>
</Link>
</p>
</div>
)
}
export default SignUp
By default, the user must verify his email address before fully signing up. You can change this setting from your Nhost dashboard.
2. Sign-in
Now that new users can sign up for our application, let's see how to allow existing users to sign in with email and password.
For that, we will use the Nhost hook named useSignInEmailPassword
inside our
SignIn
component the same way we did with our SignUp
component. So, here's
what your component should look like after applying the changes for the sign-in
logic:
import styles from '../styles/components/SignIn.module.css'
import { useState } from 'react'
import { useRouter } from 'next/router'
import { useSignInEmailPassword } from '@nhost/nextjs'
import Link from 'next/link'
import Image from 'next/image'
import Input from './Input'
import Spinner from './Spinner'
const SignIn = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const router = useRouter()
const { signInEmailPassword, isLoading, isSuccess, needsEmailVerification, isError, error } =
useSignInEmailPassword()
const handleOnSubmit = async (e) => {
e.preventDefault()
await signInEmailPassword(email, password)
}
if (isSuccess) {
router.push('/')
return null
}
const disableForm = isLoading || needsEmailVerification
return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles['logo-wrapper']}>
<Image src="/logo.svg" alt="logo" layout="fill" objectFit="contain" />
</div>
{needsEmailVerification ? (
<p className={styles['verification-text']}>
Please check your mailbox and follow the verification link to verify your email.
</p>
) : (
<>
<form onSubmit={handleOnSubmit} className={styles.form}>
<Input
type="email"
label="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={disableForm}
required
/>
<Input
type="password"
label="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={disableForm}
required
/>
<button type="submit" disabled={disableForm} className={styles.button}>
{isLoading ? <Spinner size="sm" /> : 'Sign in'}
</button>
{isError ? <p className={styles['error-text']}>{error?.message}</p> : null}
</form>
</>
)}
</div>
<p className={styles.text}>
No account yet?{' '}
<Link href="/sign-up">
<a className={styles.link}>Sign up</a>
</Link>
</p>
</div>
)
}
export default SignIn
3. Sign-out
Finally, to allow the users to sign out from the app, we can use the Nhost
useSignOut
hook:
import { useSignOut } from '@nhost/nextjs'
const Layout = ({ children = null }) => {
const { signOut } = useSignOut()
const menuItems = [
//..
{
label: 'Logout',
onClick: signOut,
icon: LogoutIcon
}
]
//...
}
Protect routes
Now that we have implemented authentication, we can easily decide who can access certain parts of our application.
In our case, we'll only allow authenticated users to have access to the /
and
/profile
routes. All the other users should be redirected to the /sign-in
page if they try to access those routes.
To do so, we can check the authentication status of the current user using the Nhost SDK by creating a high-order component:
import styles from './styles/pages/ProtectedRoute.module.css'
import { useRouter } from 'next/router'
import { useAuthenticationStatus } from '@nhost/nextjs'
import Spinner from './components/Spinner'
export default function withAuth(Component) {
return function AuthProtected(props) {
const router = useRouter()
const { isLoading, isAuthenticated } = useAuthenticationStatus()
if (isLoading) {
return (
<div className={styles.container}>
<Spinner />
</div>
)
}
if (!isAuthenticated) {
router.push('/sign-in')
return null
}
return <Component {...props} />
}
}
Then, wrap our Next.js pages, index.js
and profile.js
, with it:
- pages/index.js
- pages/profile.js
import withAuth from '../withAuth'
const Home = () => {
//...
}
export default withAuth(Home)
import withAuth from '../withAuth'
const Profile = () => {
//...
}
export default withAuth(Profile)
Retrieve user data
Finally, let's display the information of the authenticated user throughout his dashboard to make the app more personalized.
Getting the current authenticated user data is quite easy. We
can use the useUserData
hook provided by Nhost to do it.
So, open the UserProvider.js
file and use this hook like so:
import React, { useContext } from 'react'
// highlight-next-line
import { useUserData } from '@nhost/nextjs'
const UserContext = React.createContext(null)
export function UserProvider({ children = null }) {
// highlight-next-line
const user = useUserData()
return <UserContext.Provider value={{ user }}>{children}</UserContext.Provider>
}
export function useUserContext() {
return useContext(UserContext)
}
That's it! The JSX code for rendering the user data (email, display name, etc.) is already included in your components as part of the example repository you've bootstrapped at the beginning of this guide.
Update user data
Nhost provides a GraphQL API through Hasura so that we can query and mutate our data instantly.
In this tutorial, we'll use the Apollo GraphQL client for interacting with this GraphQL API.
So, start by installing the following dependencies:
- npm
- Yarn
npm install @nhost/react-apollo @apollo/client
yarn add @nhost/react-apollo @apollo/client
Then, add the NhostApolloProvider
from @nhost/react-apollo
into your
_app_.js
file.
import { NhostApolloProvider } from '@nhost/react-apollo'
function MyApp({ Component, pageProps }) {
return (
<NhostProvider nhost={nhost} initial={pageProps.nhostSession}>
<NhostApolloProvider nhost={nhost}>{/* ... */}</NhostApolloProvider>
</NhostProvider>
)
}
From there, we can construct our GraphQL query and use the Apollo useMutation
hook to execute that query when the user submits the form from the profile page:
import { gql, useMutation } from '@apollo/client'
import { toast } from 'react-hot-toast'
const UPDATE_USER_MUTATION = gql`
mutation ($id: uuid!, $displayName: String!, $metadata: jsonb) {
updateUser(pk_columns: { id: $id }, _set: { displayName: $displayName, metadata: $metadata }) {
id
displayName
metadata
}
}
`
const Profile = () => {
const [mutateUser, { loading: updatingProfile }] = useMutation(UPDATE_USER_MUTATION)
const updateUserProfile = async (e) => {
e.preventDefault()
try {
await mutateUser({
variables: {
id: user.id,
displayName: `${firstName} ${lastName}`.trim(),
metadata: {
firstName,
lastName
}
}
})
toast.success('Updated successfully', { id: 'updateProfile' })
} catch (error) {
toast.error('Unable to update profile', { id: 'updateProfile' })
}
}
//...
}
Finally, since Hasura has an allow nothing by default policy, and we haven't set any permissions yet, our GraphQL mutations would fail.
So, open the Hasura console from the Data tab of your project from your Nhost dashboard. Then, go to the permissions tab of the users
table, type in user
in the role
cell, and click the edit icon on the select
operation:
To restrict the user to read his data only, specify a condition with the
user's ID and the X-Hasura-User-ID
session variable, which is passed with each
requests.
Next, select the columns you'd like the users to have access to, and click Save Permissions.
Repeat the same steps on the update
operation for the user
role to allow
users to update their displayName
and metadata
only.
Finally, to add caching, synchronizing, and updating server state in your Next.js app, let's refactor the user data fetching by using the Apollo client and our GraphQL API instead.
So, first add the following GraphQL query to retrieve the current user data from the UserProvider.js
file:
import { gql } from '@apollo/client'
const GET_USER_QUERY = gql`
query GetUser($id: uuid!) {
user(id: $id) {
id
email
displayName
metadata
avatarUrl
}
}
`
export function UserProvider() {
//...
}
Then, replace the useUserData
hook with the useUserId
hook to retrieve the current user's ID only.
import { useUserId } from '@nhost/nextjs'
export function UserProvider() {
const id = useUserId()
//...
}
Finally, we can run our GraphQL query using the useQuery
hook and the current user's ID.
// highlight-next-line
import { gql, useQuery } from '@apollo/client'
export function UserProvider({ children = null }) {
const id = useUserId()
// highlight-start
const { loading, error, data } = useQuery(GET_USER_QUERY, {
variables: { id },
skip: !id
})
const user = data?.user
// highlight-end
// highlight-start
if (error) {
return <p>Something went wrong. Try to refresh the page.</p>
}
if (loading) {
return null
}
// highlight-end
return <UserContext.Provider value={{ user }}>{children}</UserContext.Provider>
}
You now have a fully functional Next.js application. Congratulations!