Now that we created the model that uses the DB to serve the data, it’s time to use it!

First, I’m going to manually add the houses data we had in the file to the database, creating 2 rows in the houses table:

And I added a review to one house.

Now, the pages/houses.json file now is imported by 2 files:

  • pages/houses/[id].js
  • pages/index.js

They have 2 different functions. The first is used to build the detail page, the second for the pages list.

Let’s work on the list page first.

Houses list

Instead of loading the houses from the JSON file, we’re going to add a server-side JSON endpoint, which has the job to get the houses data from the database, and to send them as a response.

Let’s add a GET /api/houses endpoint to the server.js file:

server.js

server.get('/api/houses', (req, res) => {
	House.findAndCountAll().then(result => {
		const houses = result.rows.map(house => house.dataValues)

		res.writeHead(200, {
			'Content-Type': 'application/json'
		})
		res.end(JSON.stringify(houses))
	})
})

Now if we try using Insomnia to call this endpoint, we’ll get the houses data back, referenced from our model, which in turns queries the database.

We can call this endpoint in our Next.js page, by adding a request into the getInitialProps() method, in the pages/index.js file.

We have a getInitialProps() method in the pages/houses/[id].js file, but not in the pages/index.js file, so let’s add it!

Now pages/index.js contains this code:

pages/index.js

import houses from './houses.json'
import House from '../components/House'
import Layout from '../components/Layout'

const content = (
  <div>
    <h2>Places to stay</h2>

    <div className='houses'>
      {houses.map((house, index) => {
        return <House key={index} {...house} />
      })}
    </div>

    <style jsx>{`
      .houses {
        display: grid;
        grid-template-columns: 50% 50%;
        grid-template-rows: 300px 300px;
        grid-gap: 40px;
      }
    `}</style>
  </div>
)

const Index = () => <Layout content={content} />

export default Index

We’re going to first install isomorphic-unfetch:

npm install isomorphic-unfetch

This is a package essential for our use with Next.js, because it makes fetch work on the Node.js side, transparently. Our getInitialProps function will run both in the Node.js context, when the page is server rendered, or in the client context when the page is client rendered.

Once we import it, we can use fetch() as usual

See my Fetch API tutorial at https://flaviocopes.com/fetch-api/

Here’s the new pages/index.js file content:

pages/index.js

import fetch from 'isomorphic-unfetch'
import House from '../components/House'
import Layout from '../components/Layout'

const Index = props => {
  return (
    <Layout
      content={
        <div>
          <h2>Places to stay</h2>

          <div className='houses'>
            {props.houses.map((house, index) => {
              return <House key={index} {...house} />
            })}
          </div>

          <style jsx>{`
            .houses {
              display: grid;
              grid-template-columns: 50% 50%;
              grid-template-rows: 300px 300px;
              grid-gap: 40px;
            }
          `}</style>
        </div>
      }
    />
  )
}

Index.getInitialProps = async () => {
  const res = await fetch(`http://localhost:3000/api/houses`)
  const houses = await res.json()
  return {
    houses
  }
}

export default Index

See, in Index.getInitialProps we make a fetch call to http://localhost:3000/api/houses, the API we just defined. The domain and port are hardcoded, because we need the full path. It’s a (very) good idea to take that and extraxt it in an environment variable, so you can customize the URL without editing the code.

I also dropped the const content part, because without having props used in the JSX directly, I found that getInitialProps() was not being called at all (a Next.js gotcha).

You should be able to see the pages list now, again!

I don’t handle pagination, which is a problem when you’ll have lots of houses, to not introduce too much complexity at once.

Single house

Let’s add a second GET endpoint to the server.js file, this time we define the /api/houses/:id endpoint, used to get a single house data:

server.js

server.get('/api/houses/:id', (req, res) => {
	const { id } = req.params

	House.findByPk(id).then(house => {
		if (house) {
			res.writeHead(200, {
				'Content-Type': 'application/json'
			})
			res.end(JSON.stringify(house.dataValues))
		} else {
			res.writeHead(404, {
				'Content-Type': 'application/json'
			})
			res.end(
				JSON.stringify({
					message: `Not found`
				})
			)
		}
	})
})

Let’s make use of this endpoint in the pages/houses/[id].js file.

Drop the line

import houses from '../houses.json'

and add, on top:

import fetch from 'isomorphic-unfetch'

And change House.getInitialProps to:

pages/houses/[id].js

House.getInitialProps = async ({ query }) => {
  const { id } = query

  const res = await fetch(`http://localhost:3000/api/houses/${id}`)
  const house = await res.json()

  return {
    house
  }
}

Great! You should now be able to see the house detail, and you can remove the pages/houses.json file.

Let’s also drop

<p>
	{props.house.rating} ({props.house.reviewsCount})
</p>

from pages/houses/[id].js, because we don’t have those fields any more in the database, we’ll need to calculate them from reviews.

Now reviews are stored into their own table

Let’s change the GET /api/houses/:id endpoint to add reviews to the house data, by importing the Review model and calling its findAndCountAll() method, passing the house id as the houseId column of the reviews table.

server.js

server.get('/api/houses/:id', (req, res) => {
	const { id } = req.params

	House.findByPk(id).then(house => {
		if (house) {
			Review.findAndCountAll({
				where: {
					houseId: house.id
				}
			}).then(reviews => {
				house.dataValues.reviews = reviews.rows.map(
					review => review.dataValues
				)
				house.dataValues.reviewsCount = reviews.count
				res.writeHead(200, {
					'Content-Type': 'application/json'
				})
				res.end(JSON.stringify(house.dataValues))
			})
		} else {
			res.writeHead(404, {
				'Content-Type': 'application/json'
			})
			res.end(
				JSON.stringify({
					message: `Not found`
				})
			)
		}
	})
})

I’m iterating over the result to filter just the data, using

pages/houses/[id].js

props.house.reviews.map((review, index) => {}

and then I attach the reviews to the data sent to the response.

Now going back to [id].js, I can print the reviews information in this way:

pages/houses/[id].js

{props.house.reviewsCount ? (
	<div className='reviews'>
		<h3>{props.house.reviewsCount} Reviews</h3>

		{props.house.reviews.map((review, index) => {
			return (
				<div key={index}>
					<p>{new Date(review.createdAt).toDateString()}</p>
					<p>{review.comment}</p>
				</div>
			)
		})}
	</div>
) : (
	<></>
)}

I removed the avatar and reviewer name, because I’d need another query to the database. It’s a tradeoff, I can go on getting data if I want of course, but I don’t think we need this data now. I showed the review date, which is now stored in the createdAt field.

This is the result:

The code for this lesson is available at https://github.com/flaviocopes/airbnb-clone-react-nextjs/commit/985da5a62107ca0a309ad99d9cc23edbd52e301b


Go to the next lesson