Using Neo4j in your next Next.js Project
Tags: javascript, webdev, nextjs, neo4j
Published: 2022-10-27 16:38:36

After watching a few glossy new videos from the [Next.js Conf 2022](https://nextjs.org/conf), I thought I'd take a closer look at Next.js and see how the framework could help me to build my next Neo4j-based web application.

Fundamentally, adding Neo4j integration to a Next.js project is similar to any other Node.js/TypeScript-based project.  However, the various Data Fetching methods and both Server-side and Client-side rendering raise some interesting challenges.

Let's take a look at how we can use Neo4j in a Next.js project.


## What is Next.js?

Next.js is a React-based framework which provides an opinionated starting point for building web applications.  The framework provides building blocks for many of the common features that developers need to consider when building modern applications such as UI components, Data Fetching, and Rendering.

The framework also focuses on performance, providing the ability to pre-generate static HTML pages using _Static-site Generation (SSG)_, render HTML on the server at request time using _Server-side rendering (SSR)_ and also render React components on the client-side using _Client-side Rendering (CSR)_.

You can [read more about Next.js here](https://nextjs.org/learn/foundations/about-nextjs/what-is-nextjs).


## What is Neo4j?

The chances are, if you have found this article via search, you'd know more about Next.js than Neo4j.  Neo4j is a _Graph Database_,  a database consisting of _Nodes_ - which represent entities or _things_, connected together and Relationships.

Neo4j comes into its own when working with highly connected datasets or as an alternative for complex relational database schemas where many joins are required.  The golden rule is that if your queries have three or more joins, you should really be looking at using a graph database.

You can [read more about Neo4j here](https://neo4j.com/developer/graph-database/).

## Why Neo4j and Next.js?

Next.js is gaining momentum as one of the most popular frameworks for building modern web applications.  The benefit of using Next.js is that your front-end and back-end code are all self-contained within the same subfolders of the `api/` directory.

If you are building a Neo4j-backed project, building an integration with the [Neo4j JavaScript Driver](https://github.com/neo4j/neo4j-javascript-driver/) is relatively straightforward.  All you need to do is create a new instance of the driver within the application, then use the driver to execute Cypher statements and retrieve results. 

Of course, you can use the Neo4j JavaScript driver directly from React components, but this means exposing database credentials through the client which can be a security risk.  Instead, if you require on-demand data from Neo4j in client-side rendering, you can create an API handler to execute the Cypher statement server-side and return results.


## Creating a free Neo4j AuraDB Instance

[Neo4j AuraDB](https://neo4j.com/cloud/platform/aura-graph-database/), Neo4j's fully managed cloud service provides one **AuraDB Free** instance to all users, completely free and no credit card is required.

If you sign in or register for Neo4j Aura at [cloud.neo4j.io](https://cloud.neo4j.io), you will see a **New Instance** button at the top of the screen.  If you click this button, you will be able to choose between an empty database or one pre-populated with sample data.

![Create an instance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0bt439mmq36kny36ulvr.png)

For this article, I suggest choosing the `Graph-based Recommendations` dataset, which consists of Movies, Actors, Directors and user ratings.  This dataset is a nice introduction to graph concepts and can be used to build a movie recommendation algorithm.  We use it across [GraphAcademy](https://graphacademy.neo4j.com/?ref=dev.to), including the [Building Neo4j Applications with Node.js](https://graphacademy.neo4j.com/courses/app-nodejs/?ref=dev.to) course.

Click **Create** to create your instance.  Once you have done so, a modal window will appear with a generated password.

![Aura Credentials](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ft5c38bqx9w99a7k09l.png)

Click the **Download** button to download your credentials, we'll need these a little later on.  After a couple of minutes, your instance will be ready to explore.  You can click the **Explore** button to [explore the graph with Neo4j Bloom](https://neo4j.com/product/bloom/), or query the graph using Cypher by clicking the **Query tab**.

![Neo4j AuraDB Instance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b168jaf9v0xyolzckc08.png)

You can take a look at that in your own time, for now, let's focus on our Next.js application.


## Creating a new Next.js Project

You can create a new Next.js project from a template using the [Create Next App CLI command](https://nextjs.org/docs/api-reference/create-next-app).

```sh
npx create-next-app@latest
```

The command will prompt you for a project name and install any dependencies.


## Adding Neo4j Helper Functions

To install the Neo4j JavaScript Driver, first install the dependency:

```sh
npm install --save neo4j-driver
# or yarn add neo4j-driver
```

Next.js comes with [built-in support for Environment Variables](https://nextjs.org/docs/basic-features/environment-variables), so we can simply copy the credentials file downloaded from the Neo4j Aura Console above, rename it to `.env` and place in the directory root.

We can then access those variables through the `process.env` variable:

```js
const { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD } = process.env
```

Next, create a new folder called `lib/` and then create a new `neo4j.js` file.  You will want to import the `neo4j` object from the `neo4j-driver` dependency and use the credentials above to create a driver instance

```js
// lib/neo4j.js
const driver = neo4j.driver(
  process.env.NEO4J_URI,
  neo4j.auth.basic(
    process.env.NEO4J_USERNAME,
    process.env.NEO4J_PASSWORD
  )
)
```

When executing a Cypher statement against a Neo4j instance, you need to open a session, and execute the statement within a read or write transaction.  This can become a bit cumbersome after a while, so instead, I recommend writing helper functions for read and write queries:

```js
// lib/neo4j.js
export async function read(cypher, params = {}) {
  // 1. Open a session
  const session = driver.session()

  try {
    // 2. Execute a Cypher Statement
    const res = await session.executeRead(tx => tx.run(cypher, params))

    // 3. Process the Results
    const values = res.records.map(record => record.toObject())

    return values
  }
  finally {
    // 4. Close the session 
    await session.close()
  }
}

export async function write(cypher, params = {}) {
  // 1. Open a session
  const session = driver.session()

  try {
    // 2. Execute a Cypher Statement
    const res = await session.executeWrite(tx => tx.run(cypher, params))

    // 3. Process the Results
    const values = res.records.map(record => record.toObject())

    return values
  }
  finally {
    // 4. Close the session 
    await session.close()
  }
}
```

If you want a deeper dive into this code or best practices I recommend that you check out the [Neo4j & Node.js Course](https://graphacademy.neo4j.com/courses/app-nodejs/?ref=dev.to) on GraphAcademy.

Now that we have a way to query Neo4j, let's look at the options for Data Fetching in Next.js

## Data Fetching in Next.js

Next.js allows the rendering of content in several ways.

1. Static-site Generation (SSG) - where static HTML pages are generated at _build_ time
2. Server-side Rendering (SSR) - HTML is generated server-side as a request comes in
3. Client-side Rendering (CSR) - HTTP requests are executed in the browser with JavaScript and the response updates the DOM

Depending on your use case, you may need a mixture of these methods.  Say you are running a movie recommendation site, it may make sense to use SSG to pre-build marketing pages.  Movie information is held in a database and changes regularly, so these pages should be rendered by the server using SSR.  When a user comes to rate a movie, the interaction should take place via an API request and the result rendered using CSR.

Let's take a look at the implementation of each of these records.

### Static Page Generation

Let's say, for example, that generic genre pages won't change often and they don't require any user interaction.  By generating static pages, we can serve cached versions of the pages and take the load away from the server.

Any component in the `pages/` directory which exports a `getStaticProps()` function (known as a Page) will be generated at build time and served as a static file.

Components created in the pages folder will automatically be mapped to a route.  To create a page that will be available at `/genres` you will need to create a `pages/genres/index.jsx` file.  The component needs to export a `default` function which returns a JSX component, and a `getStaticProps()` function.

First, to get the data required by the component, create the `getStaticProps()` function and execute [this Cypher statement](https://github.com/neo4j-graphacademy/neoflix-cypher/blob/main/cypher/3-backlog/1-browse-genres/genre-details.cypher) in a _read_ transaction.

```js
// pages/genres/index.jsx
export async function getStaticProps() {
  const res = await read(`
    MATCH (g:Genre)
    WHERE g.name <> '(no genres listed)'

    CALL {
    WITH g
    MATCH (g)<-[:IN_GENRE]-(m:Movie)
    WHERE m.imdbRating IS NOT NULL AND m.poster IS NOT NULL
    RETURN m.poster AS poster
    ORDER BY m.imdbRating DESC LIMIT 1
    }

    RETURN g {
      .*,
      movies: toString(size((g)<-[:IN_GENRE]-(:Movie))),
      poster: poster
    } AS genre
    ORDER BY g.name ASC
  `)

  const genres = res.map(row => row.genre)

  return {
    props: {
      genres,
    }
  }
}
```

Anything returned inside `props` from this function will be passed as a prop into the default component.

Now, export a default function which displays a list of Genres.

```jsx
// pages/genres/index.jsx
export default function GenresList({ genres }) {
  return (
    <div>
      <h1>Genres</h1>

      <ul>
        {genres.map(genre => <li key={genre.name}>
          <Link href={`/genres/${genre.name}`}>{genre.name} ({genre.movies})</Link>
        </li>)}
      </ul>
    </div>
  )
}
```

This should generate an unordered list of links for each Genre:
![Genre List](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i0862ky56e8offc2rk4m.png)

Looking good...

If you run the `npm run build` command, you will see a `genres.html` file inside the `.next/server/pages/` directory.

### Using Neo4j for Server-side Rendering

The movie list on each genre page may change often, or you may wish to add extra interaction to the page.  In this case, it makes sense to render this page on the server.  By default, Next.js will cache this page for a short amount of time which is perfect for websites with high amounts of traffic.

Each genre link on the previous page links to `/genres/[name]` - for example `/genres/Action`. By creating a `pages/genres/[name].jsx` file, Next.js knows automatically to listen for requests on any URL starting with `/genres/` and detect anything after the slash as a `name` URL parameter.

This can be accessed by the `getServerSideProps()` function, which will instruct Next.js to render this page using Server-side Rendering as the request comes in.

The `getServerSideProps()` function should be used to get the data required to render the page and return it inside a `props` key.

```js
export async function getServerSideProps({ query, params }) {
  const limit = 10
  const page = parseInt(query.page ?? '1')
  const skip = (page - 1) * limit

  const res = await read(`
    MATCH (g:Genre {name: $genre})
    WITH g, size((g)<-[:IN_GENRE]-()) AS count

    MATCH (m:Movie)-[:IN_GENRE]->(g)
    RETURN
      g { .* } AS genre,
      toString(count) AS count,
      m {
        .tmdbId,
        .title
      } AS movie
    ORDER BY m.title ASC
    SKIP $skip
    LIMIT $limit
  `, {
    genre: params.name,
    limit: int(limit),
    skip: int(((query.page || 1)-1) * limit)
  })

  const genre = res[0].genre
  const count = res[0].count

  return {
    props: {
      genre,
      count,
      movies: res.map(record => record.movie),
      page, skip, limit,
    }
  }
}
```

In the example above, I get the movie name from the `params` object in the request context which is passed as the only argument to the `getServerSideProps()` function.  I also attempt to get the `?page=` query parameter from the URL to provide a paginated list of movies.

These values will again be passed as props into the _default_ function, and can therefore be used to list the movies and pagination links.


```jsx
export default function GenreDetails({ genre, count, movies, page, skip, limit }) {
  return (
    <div>
      <h1>{genre.name}</h1>
      <p>There are {count} movies listed as {genre.name}.</p>


      <ul>
        {movies.map(movie => <li key={movie.tmdbId}>{movie.title}</li>)}
      </ul>

      <p>
        Showing page #{page}. <br />
        {page > 1 ? <Link href={`/genres/${genre.name}?page=${page-1}`}> Previous</Link> : ' '}
        {' '}
        {skip + limit < count ? <Link href={`/genres/${genre.name}?page=${page+1}`}>Next</Link> : ' '}
      </p>

    </div>
  )
}
```

Next.js then renders a list of movies with each request.

![A list of Movies in the Adventure genre](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oy4nu7m8iksu8vqprnt0.png)


### Using Neo4j for Client-side Data Fetching

As it stands, for each click of the Previous and Next links above, the entire page will reload which isn't ideal.  Although this is a trivial example so far, loading KBs worth of HTML again to render the header and footer means additional load on the server and more data sent over the wire.

Instead, you could build a React component that would load the list of movies asynchronously through a client-side HTTP request.  This would mean that the list of movies could be updated without reloading the entire page, providing the end-user with a smoother viewing experience.

To support this, we will have to create a [API Route](https://nextjs.org/docs/api-routes/introduction) which will return a list of movies as JSON.

Any file in the `pages/api/` directory is treated as a route handler, a single default exported function which accepts request and response parameters, and expects an HTTP status and response to be returned.

So to create an API route to serve a list of movies at `http://locahost:3000/api/movies/[name]/movies`, create a new `movies.js` file in the `pages/api/genres/[name]` folder.

```js
// pages/api/genres/[name]/movies.js
export default async function handler(req, res) {
  const { name } = req.query
  const limit = 10
  const page = parseInt(req.query.page as string ?? '1')
  const skip = (page - 1) * limit

  const result = await read<MovieResult>(`
    MATCH (m:Movie)-[:IN_GENRE]->(g:Genre {name: $genre})
    RETURN
      g { .* } AS genre,
      toString(size((g)<-[:IN_GENRE]-())) AS count,
      m {
        .tmdbId,
        .title
      } AS movie
    ORDER BY m.title ASC
    SKIP $skip
    LIMIT $limit
  `, {
      genre: name,
      limit: int(limit),
      skip: int(skip)
  })

  res.status(200).json({
    total: parseInt(result[0]?.count) || 0,
    data: result.map(record => record.movie)
  })
}
```

The function above executes a Cypher statement in a read transaction, processes the results and returns the list of 
movies as a JSON response.

A quick GET request to http://localhost:3000/api/genres/Action/movies shows a list of movies:

```json
[
  {
    "tmdbId": "72867",
    "title": "'Hellboy': The Seeds of Creation"
  },
  {
    "tmdbId": "58857",
    "title": "13 Assassins (Jûsan-nin no shikaku)"
  },
  /* ... */
]
```

This API handler can then be called through a React component in a `useEffect` hook.

```jsx
// components/genre/movie-list.tsx
export default function GenreMovieList({ genre }: GenreMovieListProps) {
  const [page, setPage] = useState<number>(1)
  const [limit, setLimit] = useState<number>(10)
  const [movies, setMovies] = useState<Movie[]>()
  const [total, setTotal] = useState<number>()

  // Get data from the API
  useEffect(() => {
    fetch(`/api/genres/${genre.name}/movies?page=${page}&limit=${limit}`)
      .then(res => res.json())
      .then(json => {
        setMovies(json.data)
        setTotal(json.total)
      })


  }, [genre, page, limit])


  // Loading State
  if (!movies || !total) {
    return <div>Loading...</div>
  }

  return (
    <div>
      <ul>
        {movies.map(movie => <li key={movie.tmdbId}>{movie.title}</li>)}
      </ul>

      <p>Showing page {page}</p>

      {page > 1 && <button onClick={() => setPage(page - 1)}>Previous</button>}
      {page * limit < total && <button onClick={() => setPage(page + 1)}>Next</button>}
    </div>
  )
}
```

The component is then in charge of pagination and any update to the list doesn't re-render the entire page.



## Conclusion

This is far from a comprehensive guide to Next.js or Neo4j integrations but hopefully, it serves as a quick reference for anyone wondering the best way to integrate Neo4j, or any other database for that matter, with a Next.js application.

All of the code from this experiment is [available on Github](https://github.com/adam-cowley/neo4j-nextjs-example).

If you are interested in learning more about Next.js, they have put together a [course for developers to learn the basics](https://nextjs.org/learn/basics/create-nextjs-app).

If you would like to learn more about Neo4j, then I would recommend taking a look at the [Beginners Neo4j Courses on GraphAcademy](https://graphacademy.neo4j.com?ref=dev.to).  If you want to know more about how to use the Neo4j JavaScript Driver in a Node.js or Typescript project, I would also recommend the [Building Neo4j Applications with Node.js course](https://github.com/neo4j-graphacademy/app-nodejs?ref=dev.to).

If you have any comments or questions, feel free to [reach out to me on Twitter](https://twitter.com/adamcowley).