Search justacoding.blog. [Enter] to search. Click anywhere to close.

January 12th, 2022

Implement Node/Express Sessions With Postgres

In this tutorial or guide, we’ll be implementing a Node/Express API as well as utilizing a Postgres database.

Alongside this, we’ll create a single-page React app to consume this new API. The user will be able to authenticate with the app and have their session persisted.

After following along with this article, you’ll have the basis of a new application whereby it’s possible for users to:

  • Register
  • Log in
  • Log out
  • Their details will be persisted (server-side) and their login state (or session) will be handled gracefully within the React single-page app

It would then be possible for you to begin implementing the remainder of your business logic on top of this boilerplate/sample application, as per your own requirements.

Before you start – some assumptions…

This brief tutorial assumes the following:

  • You have a working Node environment
  • You have a basic React app that you’re ready to build upon (you can use Create React App for that, if not)
  • You have a working Postgres environment and database configured already

Note: the code snippets in this demonstration follow a “bare-bones” approach. There are often much better ways to handle things within a production context. For instance, you wouldn’t necessarily want to bundle all of the functionalities associated with your Express API in to the index file or entry point.

The functionality should be delegated across controllers, for instance, or things can become unwieldly and hard to maintain.

The purpose of the guide is to quickly portray the key steps with regards to handling sessions in Node and Express with Postgres, and not to provide a fully-functional production example.

With that being said, let’s make a start!

Session cookies and how this process generally works

Before we dig into the actual implementation, it’s worth briefly going over what session cookies are and how the sessions are persisted via this method.

  • The user registers (or logs in)
  • A session ID is generated and stored locally, in the cookies of the browser
  • It’s also stored server-side, in the database
  • The relevant user data can also be attached to this session data within the database
  • On subsequent requests, the session cookie is passed (remember, it’s stored locally in the user’s browser) to the server
  • Given this, the session cookie can then be used to identify the corresponding user from our database, and thus we know we’re dealing with a registered & logged in user

That’s briefly how this works (more or less) — but feel free to dig into this topic further in other articles such as Using HTTP cookies from Mozilla, which also covers session management in general.

With that said, it’s time to make a start on our implementation!

The API/backend service – Node, Express and Postgres

We’ll start by looking at the backend initially, starting with our Express API. But firstly, let’s install the required dependencies and bootstrap the project.

Install the dependencies

Here are the necessary dependencies for this project:

npm install bcryptjs connect-pg-simple cors dotenv express express-session pg

// as per my package.json...
"bcryptjs": "^2.4.3",
"connect-pg-simple": "^7.0.0",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-session": "^1.17.2",
"pg": "^8.7.1"

To quickly recap what each of these dependencies is responsible for:

  • bcrypt: password hashing
  • pg: the Postgres client
  • connect-pg-simple: a session store for use with Express/Postgres
  • cors: Express/Connect middleware for handling CORS
  • express: the web framework
  • express-session: session middleware for Express
  • dotenv: for storing environment-specific members required by your application

Create the required tables in your database

There are only two tables required as part of this sample app: the users table and the session table.

The users table

create table if not exists users (
    id serial primary key,
    firstname varchar (50) not null,
    surname varchar (50) not null,
    email varchar (100) not null,
    password varchar (200) not null,
    created_at timestamp with time zone default now(),
    constraint uk_users_email unique (email)
);

This is a fairly standard table. There’s a unique constraint on the email column, as we don’t want multiple users with the same email registering.

The session table

We also need the session table, this handles the relevant session data (as you may have guessed).

We don’t need to create this table manually. There’s a migration/script included within the connect-pg-simple package that we can utilize directly instead, via the following:

psql mydatabase < node_modules/connect-pg-simple/table.sql

You can check out the documentation if you require any more information on that.

Once both of those tables have been created within your database, we can move to the next step.

Perform the required application bootstrapping/configuration

In your main file or entrypoint (likely called index.js or server.js), you’ll need to initialize the Express app as well as all of the required middlewares and so on.

Here’s what mine looks like:

require('dotenv').config()

const express = require('express')
const cors = require('cors')
const session = require('express-session')

// express app init and config
const app = express()

app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.use(
    cors({
        origin: 'http://localhost:3001',
        methods: ['POST', 'PUT', 'GET', 'OPTIONS', 'HEAD'],
        credentials: true,
    })
)

// pg init and config
const { Client } = require('pg')
const conObject = {
    user: process.env.USER,
    host: process.env.HOST,
    database: process.env.DATABASE,
    password: process.env.PASSWORD,
    port: process.env.PORT,
}

const client = new Client(conObject)
client.connect()

// session store and session config
const store = new (require('connect-pg-simple')(session))({
    conObject,
})

app.use(
    session({
        store: store,
        secret: process.env.SESSION_SECRET,
        saveUninitialized: false,
        resave: false,
        cookie: {
            secure: false,
            httpOnly: false,
            sameSite: false,
            maxAge: 1000 * 60 * 60 * 24,
        },
    })
)

// now listen on port 3000...
const port = 3000
app.listen(port, () => {
    console.log(`App started on port ${port}`)
})

So we’re doing a number of things here:

  • Require all of the modules and dependencies that we need
  • Initialize and configure the Express app itself
  • Configure the Postgres connection
  • Configure CORS
  • Configure the session related stuff
  • And finally, listen for connections on port 3000

That’s the only required setup for this simple implementation.

With these pieces in order, it should be possible to build up the required functionality and endpoints necessary to facilitate the various authentication workflows as outlined initially.

A note on CORS…

The CORS setup is required here as I’ll be making requests from a different domain, effectively.

In my case, the Express API is running on port 3000.

My single-page React app will be running on port 3001.

So I need to allow that via the CORS configuration, as demonstrated above. We also need to account for this via the frontend form submissions, too. But we’ll cover this later on.

Without this configuration, there would be difficulty with the required session-related facilities, and the client wouldn’t get properly authenticated each time as required.

Building up the Express API endpoints

Now we’re ready to start adding the necessary endpoints to our Express API.

I’m bundling all of this code into my index.js file for the sake of ease and demonstration, but feel free to structure your app as you please.

The user registration endpoint

This endpoint will allow new users to register within your application. Let’s build it first, then we can more easily build (and test) the log in endpoint afterwards.

Let’s briefly summarize how this user registration process will work, for the sake of completeness:

  • The user submits a request to our endpoint
  • They supply firstname, surname, email and a password params
  • We insert the new user record into the users table, creating a hashedPassword using bcrypt
  • We modify the session to store the user details alongside it, ensuring that the user is automatically logged in upon completing the registration process
  • We return the user JSON to the client also

Implementing the user registration process

Here’s the full implementation with regards to the user registration endpoint of the Express API:

app.post('/register', async (req, res) => {
    const { firstname, surname, email, password } = req.body

    if (
        firstname == null ||
        surname == null ||
        email == null ||
        password == null
    ) {
        return res.sendStatus(403)
    }

    try {
        const hashedPassword = bcrypt.hashSync(req.body.password, 10)
        const data = await client.query(
            'INSERT INTO users (firstname, surname, email, password) VALUES ($1, $2, $3, $4) RETURNING *',
            [firstname, surname, email, hashedPassword]
        )

        if (data.rows.length === 0) {
            res.sendStatus(403)
        }
        const user = data.rows[0]

        req.session.user = {
            id: user.id,
            firstname: user.firstname,
            surname: user.surname,
            email: user.email,
        }

        res.status(200)
        return res.json({ user: req.session.user })
    } catch (e) {
        console.error(e)
        return res.sendStatus(403)
    }
}

There’s some basic error handling followed by an insert into the users table.

The key step after that involves the use of req.session. This session member has been attached via the express-session middleware we configured previously.

Upon successful registration, we want to log the user in automatically, that’s why we are modifying the session at this point.

As you can see, the user data is attached to the session object.

We’ll use this later on (in the client app) to identify logged in users. You’ll see that the user object is returned via this endpoint as a JSON object, also — provided the registration was indeed successful.

The login endpoint

This endpoint will allow previously-registered used to log in to your application, provided their supplied credentials are consistent with the details we have stored in our Postgres database.

Here’s how the login flow works:

  • The user submits a request to our endpoint
  • They supply the email and password params
  • We look for that user in our users table
  • Then we compare the passwords — the supplied password with the stored password. We use bcrypt to compare these, as one password (the stored one) is hashed
  • We modify the session to store the user details alongside it
  • We return the user JSON data to the client

Implementing the login process

Here’s the login implementation, it’s similar in structure to the registration one.

app.post('/login', async (req, res) => {
    const { email, password } = req.body

    if (email == null || password == null) {
        return res.sendStatus(403)
    }

    try {
        const data = await client.query(
            'SELECT id, firstname, surname, email, password FROM users WHERE email = $1',
            [email]
        )

        if (data.rows.length === 0) {
            return res.sendStatus(403)
        }
        const user = data.rows[0]

        const matches = bcrypt.compareSync(password, user.password)
        if (!matches) {
            return res.sendStatus(403)
        }

        req.session.user = {
            id: user.id,
            firstname: user.firstname,
            surname: user.surname,
            email: user.email,
        }

        res.status(200)
        return res.json({ user: req.session.user })
    } catch (e) {
        console.error(e)
        return res.sendStatus(403)
    }
}

So again, there’s some basic error handling.

Next, we look for the user in our database (via their email).

If the user exists, we need to compare their supplied password with the actual, stored password. We need to use bcrypt for this for the reasons outlined previously.

If all is successful, we set the user object alongside the session and also return the user object to the consumer. The various error handling and so on can be handled as per your own requirement. For now, I’m just returning status codes to indicate what has happened in each case.

The logout endpoint

To log the user out of the application, we just need to destroy the active session, like so:

app.post('/logout', async (req, res) => {
    try {
        await req.session.destroy()
        return res.sendStatus(200)
    } catch (e) {
        console.error(e)
        return res.sendStatus(500)
    }
}

Provided the operation was successful, only a 200 response is returned here. It’s down to the consumer (the React single-page app) to handle this (ie. remove the necessary login state client-side).

We’ll go over how that looks shortly, once we look at implementing the frontend components.

An endpoint to fetch user details/the current session

Here’s the last endpoint we’ll be needing.

The purpose of this endpoint is to fetch the “current” user from the given session, and return that data as JSON.

You’ll see more about how and why we need this in the next step when we look at integrating this API with the React frontend.

app.post('/fetch-user', async (req, res) => {
    if (req.sessionID && req.session.user) {
        res.status(200)
        return res.json({ user: req.session.user })
    }
    return res.sendStatus(403)
})

We’re basically looking at the given session for the stored user details.

If those exist, we know that we have an active session that’s associated with one of our registered users, and thus, we can handle this scenario accordingly in the React app.

The frontend portion – the React app

Now we (hopefully) have a working Node/Express API that is properly communicating with our Postgres database, we can begin to integrate this with our single-page React app.

Let’s start with the GlobalContextProvider first and foremost. This is used to pin the app together and contains some key global state (actually, it’s only really handling the user stuff currently).

If you’re unfamiliar with contexts in React, it’s well worth a read beforehand (though it’s not strictly required at this point — but my sample application does use it).

It’s worth nothing that in a real-world application, it’d likely be a better idea to delegate all auth (and session) related functionalities to their own file, following your preferred structure as you see fit.

However, here’s my context provider:

import React, { useState, useEffect } from 'react'
import GlobalContext from './GlobalContext'
const axios = require('axios')

const GlobalContextProvider = ({ children }) => {
    const [user, setUser] = useState({})
    const [fetchingUser, setFetchingUser] = useState(true)

    useEffect(() => {
        axios
            .post(`${process.env.REACT_APP_BASE_URL}/fetch-user`, null, {
                withCredentials: true,
                headers: {
                    'Access-Control-Allow-Origin': '*',
                },
            })
            .then((response) => {
                console.log(`Fetched session for user: ${response.data.user}`)
                setUser(response.data.user)
            })
            .catch((error) => {
                console.log(`No user exists with the current session... ${error}`)
            })
            .finally(() => {
                setFetchingUser(false)
            })
    }, [])

    return (
        <GlobalContext.Provider
            value={{
                user: user,
                setUser: setUser,
                fetchingUser: fetchingUser
            }}
        >
            {children}
        </GlobalContext.Provider>
    )
}

export default GlobalContextProvider

As you can see, I’m using the React Context API here to cascade state down throughout my app.

In particular, it’s handling state relevant to the user itself.

Upon the application’s initial load – we’re fetching the relevant user based on the current session.

This request could be placed anywhere in your app where it would always run once (regardless of the page) upon first load. I’m just putting the request directly inside of the context provider for now, for the sake of simplicity.

If the session exists and the user is logged in (ie. we have user data attached to the current session in our database), we know that we need to inform the React app of this. Hence, the user state is set using the returned JSON data from our backend service.

Alternatively, if the user is not currently logged in, the user state will be set to null.

This effectively means that there’s no specific user logged in, and as such, our app should account for this accordingly (show a login button instead of a logout one, obscure restricted UI elements and so on).

What’s fetchingUser for?

This is simply a loading state.

When this request is taking place initially (once the app is first opened up) we can use this boolean to apply a loading flag as required.

We do this in the Header component, later on.

The registration form

Here’s the registration form in its entirety.

Note: you can’t really copy and paste most of these components directly. I’m using other components that aren’t included or detailed in this article (such as Button).

But you can hopefully get a good idea of what’s going on, regardless:

import { useState, useContext } from 'react'
import { useNavigate } from 'react-router-dom'
import GlobalContext from '../providers/GlobalContext'
import Button from '../components/Button'
const axios = require('axios')

const Register = () => {
    const globalContext = useContext(GlobalContext)
    const navigate = useNavigate()

    const [loading, setLoading] = useState(false)
    const [error, setError] = useState()
    const [formData, setFormData] = useState({
        firstname: '',
        surname: '',
        email: '',
        password: '',
    })

    const submitForm = () => {
        setLoading(true)
        axios
            .post(`${process.env.REACT_APP_BASE_URL}/register`, formData, {
                withCredentials: true,
                headers: {
                    'Access-Control-Allow-Origin': '*',
                },
            })
            .then((response) => {
                if (response.status === 200) {
                    console.log(response.data.user)
                    globalContext.setUser(response.data.user)
                    navigate('/dashboard')
                } else {
                    throw new Error()
                }
            })
            .catch((error) => {
                setError('Registration failed')
            })
            .finally(() => {
                setLoading(false)
            })
    }

    return (
        <div className="page-register">
            <h1 className="title">Register</h1>

            {error && error}

            <input
                type="text"
                name="firstname"
                value={formData.firstname}
                onChange={(e) => setFormData({ ...formData, firstname: e.target.value })}
            />

            <input
                type="text"
                name="surname"
                value={formData.surname}
                onChange={(e) => setFormData({ ...formData, surname: e.target.value })}
            />

            <input
                type="text"
                name="email"
                value={formData.email}
                onChange={(e) => setFormData({ ...formData, email: e.target.value })}
            />

            <input
                type="password"
                name="password"
                value={formData.password}
                onChange={(e) => setFormData({ ...formData, password: e.target.value })}
            />

            <Button text="Register" loading={loading} onClick={submitForm} />
        </div>
    )
}

export default Register

This is pretty standard stuff.

It’s a basic form with a loading state along with some simple error handling.

You’ll also notice the use of the React Context API. I’m using this to cascade state down to the consuming components, but also as a place to initiate the fetchUser request from. This request could take place from a number of other places really, as long as it’s always called early and only upon initial load.

More on that later.

Allowing CORS to work

As you can see in the POST request here, I’m configuring the following options:

{
    withCredentials: true,
    headers: {
        'Access-Control-Allow-Origin': '*',
}

This allows the relevant session cookie to be correctly set as part of this request. Without this configuration, you’ll see that the cookie isn’t set within your browser, and as such, the session won’t actually function as required.

This same configuration is included in other requests where session modification needs to take place.

The login form

Here’s the login form:

import { useState, useContext } from 'react'
import { useNavigate } from 'react-router-dom'
import GlobalContext from '../providers/GlobalContext.js'
import Button from '../components/Button'
const axios = require('axios')

const Login = () => {
    const globalContext = useContext(GlobalContext)
    const navigate = useNavigate()

    const [loading, setLoading] = useState(false)
    const [error, setError] = useState()
    const [formData, setFormData] = useState({
        email: '',
        password: '',
    })

    const submitForm = () => {
        setLoading(true)
        axios
            .post(`${process.env.REACT_APP_BASE_URL}/login`, formData, {
                withCredentials: true,
                headers: {
                    'Access-Control-Allow-Origin': '*',
                },
            })
            .then((response) => {
                if (response.status === 200) {
                    console.log(response.data.user)
                    globalContext.setUser(response.data.user)
                    navigate('/dashboard')
                } else {
                    throw new Error()
                }
            })
            .catch((error) => {
                setError('Incorrect details')
            })
            .finally(() => {
                setLoading(false)
            })
    }

    return (
        <div className="page-login">
            <h1 className="title">Login</h1>

            {error && error}

            <input
                type="text"
                name="email"
                value={formData.email}
                onChange={(e) => setFormData({ ...formData, email: e.target.value })}
            />

            <input
                type="password"
                name="password"
                value={formData.password}
                onChange={(e) => setFormData({ ...formData, password: e.target.value })}
            />

            <Button text="Login" loading={loading} onClick={submitForm} />
        </div>
    )
}

export default Login

Again, there’s nothing special about this form.

Upon submission, it passes the email and password params within the request.

Now that the forms are in place…

All going well, the login process as well as the registration process should function as expected from the React app.

An easy way to verify this is to log in to your Postgres database, and ensure that a session was indeed created.

You can look at the session table for this. Expect to see a new session row with the relevant expiry date set and so on.

This session ID should correlate with the session ID stored in your browser, as a cookie, too. Inspect your cookies locally to verify that this is the case.

If it all looks good in that regard, lets move to the next step – displaying the login status as part of the user interface!

Display the login status for the current user in the header

Here’s my Header component:

import React, { useContext, useState, Fragment } from 'react'
import GlobalContext from '../providers/GlobalContext'
import { useNavigate, Link } from 'react-router-dom'
import Button from './Button.jsx'

const axios = require('axios')

const Header = () => {
    const globalContext = useContext(GlobalContext)
    const navigate = useNavigate()
    const user = globalContext.user

    const [loading, setLoading] = useState(false)

    const logout = () => {
        setLoading(true)
        axios
            .post(`${process.env.REACT_APP_BASE_URL}/logout`, null, {
                withCredentials: true,
                headers: {
                    'Access-Control-Allow-Origin': '*',
                },
            })
            .then((response) => {
                if (response.status === 200) {
                    globalContext.setUser({})
                    navigate('/')
                } else {
                    throw new Error()
                }
            })
            .catch((error) => {
                console.error(`Couldn't log the user out: ${error}`)
            })
            .finally(() => {
                setLoading(false)
            })
    }

    return (
        <div>
            {globalContext.fetchingUser ? (
                <h1 className="loader">Loading...</h1>
            ) : (
                <Fragment>
                    <h1>{user.id ? `Logged in as ${user.firstname} ${user.surname}` : 'Not logged in'}</h1>
                    {user.id ? (
                        <Button text="Logout" loading={loading} onClick={logout} />
                    ) : (
                        <Button text="Login" onClick={() => navigate('/login')} />
                    )}

                    <Link to="/dashboard">Dashboard</Link>
                    <Link to="/change-password">Change Password</Link>
                </Fragment>
            )}
        </div>
    )
}

export default Header

As you can see, the role of this Header component is to provide a navigation menu for the user (it links to Dashboard and Change Password pages currently) as well as display the user’s login status.

In terms of actually displaying this status, you’ll notice the value we need to look at comes from the GlobalContextProvider which we covered previously.

Let’s go over the logout functionality, next.

The logout button

As you can see, this functionality is fairly straightforward.

Upon clicking the logout button, the request is made. If successful, we “unset” the user in state.

More specifically, we set it to an empty object.

At this point, the following has happened:

  • The user’s session has been destroyed in the backend
  • The user’s session cookie has been unset
  • In the React app, our state (user within the GlobalContext) has been unset also

And that’s it, the user is effectively logged out of the application.

The status within this Header component should be consistent with that, all going well.

An important note…

It’s worth noting that there are many ways to handle this kind of functionality (persisting sessions on the frontend, in particular).

The route outlined in this article is simply the most straightforward in my opinion when dealing with a SPA (single-page app).

If you want to avoid extra requests (ie. fetching the current user every time the app is loaded) there are ways around that. For instance, you could look at storing the information you need locally too, just remember to correctly maintain/handle that when the login state does change.

Of course, you could also look at JWTs.

What are JWTs and should I be using them?

Perhaps the most obvious alternative is to use JSON Web Tokens instead of this cookie-based session approach.

There are both pros and cons to doing this.

I’d strongly advise doing your own research before determining which solution is most appropriate for your app. There is plenty of content out there on this very topic, for example the comparisons contained within this article Using Session Cookies Vs. JWT for Authentication from hackernoon.

In summary

I hope you’ve enjoyed this brief guide on persisting sessions using Express along with Postgres and the other various tooling we’ve covered.

As stated previously, the aim of the guide is to provide a rough idea on how you could go about implementing sessions. It’s still advisable to investigate these various aspects yourself and draw your own conclusions!

Please check back later for other articles like this one.

Thanks for reading!

Have any questions? Ping me over at @justacodingblog
← Back to blog