DEV Community

Adam Cowley
Adam Cowley

Posted on

Using Neo4j in your next Next.js Project

After watching a few glossy new videos from the Next.js Conf 2022, 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.

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.

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 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, 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, 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

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, including the Building Neo4j Applications with Node.js course.

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

Aura Credentials

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, or query the graph using Cypher by clicking the Query tab.

Neo4j AuraDB Instance

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.

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

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:

npm install --save neo4j-driver
# or yarn add neo4j-driver
Enter fullscreen mode Exit fullscreen mode

Next.js comes with built-in support for 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:

const { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD } = process.env
Enter fullscreen mode Exit fullscreen mode

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

// lib/neo4j.js
const driver = neo4j.driver(
  process.env.NEO4J_URI,
  neo4j.auth.basic(
    process.env.NEO4J_USERNAME,
    process.env.NEO4J_PASSWORD
  )
)
Enter fullscreen mode Exit fullscreen mode

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:

// 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()
  }
}
Enter fullscreen mode Exit fullscreen mode

If you want a deeper dive into this code or best practices I recommend that you check out the Neo4j & Node.js Course 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 in a read transaction.

// 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,
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

This should generate an unordered list of links for each Genre:
Genre List

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.

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,
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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

A list of Movies in the Adventure genre

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 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.

// 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)
  })
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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

// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.

If you are interested in learning more about Next.js, they have put together a course for developers to learn the basics.

If you would like to learn more about Neo4j, then I would recommend taking a look at the Beginners Neo4j Courses on GraphAcademy. 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.

If you have any comments or questions, feel free to reach out to me on Twitter.

Top comments (0)