Airbnb clone, implement model and DB connection

Join the 2022 Full-Stack Web Dev Bootcamp!


This post is part of a new series where we build a clone of Airbnb with Next.js. See the first post here.

In this lesson we’re going to add the user to the Postgres table when they fill the registration form, and we’re going to create a session.

The user will be immediately logged in when they register.

Let’s do it!

First, I want to simulate the registration form, because I don’t want to keep using the website interface to test our functionality.

I do so using Insomnia, a powerful application to work with network requests, for HTTP requests and also for GraphQL (it’s cross platform, works on Mac / Windows / Linux).

Remember that in the registration form we have those 3 fields:

  • email
  • password
  • passwordconfirmation

and we submit those to the /api/auth/register server-side endpoint, as JSON data.

Let’s replicate this in an Insomnia request:

If all went well, the server should now print the data you sent in.

Let’s make the connection to Postgres now, so we can store the data in the users table.

Install the pg and sequelize npm packages:

npm install pg sequelize

pg handles the Postgres connection. Sequelize instead is an ORM, a tool that makes working with a database much, much easier than working with plain SQL.

Create a .env.local file in the project with the connection URL provided by Railway:

POSTGRES_CONNECTION=postgresql://postgres:PASSWORD@containers-us-west-7.railway.app:6195/railway

By default this file is never pushed stored in Git. Don’t push anything to GitHub that contains your passwords, as you might accidentally make it public.

When you deploy, the platform you use (like Vercel) will provide a way to enter this environment variable through their interface.

Next, create a model.js file in the project root folder.

We initialize a Sequelize instance passing the data to access our database:

import { Sequelize, Model, DataTypes } from 'sequelize'

const sequelize = new Sequelize(process.env.POSTGRES_CONNECTION, {
  dialect: 'postgres',
  logging: false
})

We also disable logging, because it can be very verbose as it logs all the SQL queries, which we don’t really need to look at (unless you’re debugging a problem):

Then in this file we create a model for our users table, describing the data it contains and the rules we want to apply. In this case, we disable null, to always require an email and password:

export class User extends Model {}

User.init(
  {
    email: {
      type: DataTypes.STRING,
      allowNull: false
    },
    password: {
      type: DataTypes.STRING,
      allowNull: false
    },
    session_token: {
      type: DataTypes.STRING
    },
    session_expiration: {
      type: DataTypes.DATEONLY
    }
  },
  {
    sequelize,
    modelName: 'user',
    timestamps: false
  }
)

I also tell timestamps: false otherwise we’d get an error, as Sequelize expects the createdAt and updatedAt fields in the table, which we don’t have. You could add them, but it’s unnecessary I think.

Now we use this model in the pages/api/auth/register.js file. We import it from the model.js file:

import { User } from '../../../model.js'

and then we call User.create() when we get a POST request, to add the user to the database.

I use await because this method returns a promise, so I also add async to the function assigned to post:

pages/api/auth/register.js

import { User } from '../../../model.js'

export default async (req, res) => {
  if (req.method !== 'POST') {
    res.status(405).end() //Method Not Allowed
    return
  }

  const { email, password, passwordconfirmation } = req.body

  const user = await User.create({ email, password })
  res.end(JSON.stringify({ status: 'success', message: 'User added' }))
}

Great! So now if you try to send the data using Insomnia, you’ll see the data into the table:

and a JSON message back:

{
  "status": "success",
  "message": "User added"
}

I want to check if the password confirmation matches the password, and if not I’ll show an error and halt the user registration process:

pages/api/auth/register.js

if (password !== passwordconfirmation) {
  res.end(
    JSON.stringify({ status: 'error', message: 'Passwords do not match' })
  )
  return
}

Here’s the full code so far

pages/api/auth/register.js

import { User } from '../../../model.js'

export default async (req, res) => {
  if (req.method !== 'POST') {
    res.status(405).end() //Method Not Allowed
    return
  }

  const { email, password, passwordconfirmation } = req.body

  if (password !== passwordconfirmation) {
    res.end(
      JSON.stringify({ status: 'error', message: 'Passwords do not match' })
    )
    return
  }

  const user = await User.create({ email, password })
  res.end(JSON.stringify({ status: 'success', message: 'User added' }))
}

Try registering a user with a password that’s not confirmed correctly. You’ll get this error message:

{"status":"error","message":"Passwords do not match"}

We need to do something important now: we need to encrypt the password stored in the database.

You never store passwords as plain text, as it is very dangerous for your reputation and your users.

We’re going to add a configuration to Sequelize to automatically encrypt passwords using bcrypt.

Install bcrypt using npm:

npm install bcrypt

now import bcrypt in the model.js file:

import bcrypt from 'bcrypt'

and add this code to the second argument you pass to User.init() in the model.js file:

model.js

hooks: {
  beforeCreate: async (user) => {
    const saltRounds = 10
    const salt = await bcrypt.genSalt(saltRounds)
    user.password = await bcrypt.hash(user.password, salt)
  }
}

We’re also going to add a new method to the User object prototype, which we call isPasswordValid():

User.prototype.isPasswordValid = async function (password) {
  return await bcrypt.compare(password, this.password)
}

which we’ll use later on to verify the password is correct.

The full code should be:

model.js

import { Sequelize, Model, DataTypes } from 'sequelize'
import bcrypt from 'bcrypt'

const sequelize = new Sequelize(process.env.POSTGRES_CONNECTION, {
  dialect: 'postgres',
  logging: false
})

class User extends Model {}

User.init(
  {
    email: {
      type: DataTypes.STRING,
      allowNull: false
    },
    password: {
      type: DataTypes.STRING,
      allowNull: false
    },
    session_token: {
      type: DataTypes.STRING
    },
    session_expiration: {
      type: DataTypes.DATEONLY
    }
  },
  {
    sequelize,
    modelName: 'user',
    timestamps: false,
    hooks: {
      beforeCreate: async (user) => {
        const saltRounds = 10
        const salt = await bcrypt.genSalt(saltRounds)
        user.password = await bcrypt.hash(user.password, salt)
      }
    }
  }
)

User.prototype.isPasswordValid = async function (password) {
  return await bcrypt.compare(password, this.password)
}

export { sequelize, User }

If you try registering a new user, you’ll see the password is now encrypted:

In the next lesson we’ll go on with handling the session.

See the code on GitHub

Next part: Create a session token

Want to become a better Web Developer? Join the 2022 Web Development Bootcamp!

⭐️⭐️⭐️ Join the 2022 Web Development Bootcamp ⭐️⭐️⭐️