Building a Messenger App: Access Page

This post is the 7th on a series:

Now that we’re done with the backend, lets move to the frontend. I will go with a single-page application.

Lets start by creating a file static/index.html with the following content.

<!DOCTYPE html>
<html lang="en">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="shortcut icon" href="data:,">
    <link rel="stylesheet" href="/styles.css">
    <script src="/main.js" type="module"></script>

This HTML file must be server for every URL and JavaScript will take care of rendering the correct page.

So lets go the the main.go for a moment and in the main() function add the following route:

router.Handle("GET", "/...", http.FileServer(SPAFileSystem{http.Dir("static")}))
type SPAFileSystem struct {
	fs http.FileSystem

func (spa SPAFileSystem) Open(name string) (http.File, error) {
	f, err := spa.fs.Open(name)
	if err != nil {
		return spa.fs.Open("index.html")
	return f, nil

We use a custom file system so instead of returning 404 Not Found for unknown URLs, it serves the index.html.


In the index.html we loaded two files: styles.css and main.js. I leave styling to your taste.

Lets move to main.js. Create a static/main.js file with the following content:

import { guard } from './auth.js'
import Router from './router.js'

let currentPage
const disconnect = new CustomEvent('disconnect')
const router = new Router()

router.handle('/', guard(view('home'), view('access')))
router.handle('/callback', view('callback'))
router.handle(/^\/conversations\/([^\/]+)$/, guard(view('conversation'), view('access')))
router.handle(/^\//, view('not-found'))

router.install(async result => {
    document.body.innerHTML = ''
    if (currentPage instanceof Node) {
    currentPage = await result
    if (currentPage instanceof Node) {

function view(pageName) {
    return (...args) => import(`/pages/${pageName}-page.js`)
        .then(m => m.default(...args))

If you are follower of this blog, you already know how this works. That router is the one showed here. Just download it from @nicolasparada/router and save it to static/router.js.

We registered four routes. At the root / we show the home or access page whether the user is authenticated. At /callback we show the callback page. On /conversations/{conversationID} we show the conversation or access page whether the user is authenticated and for every other URL, we show a not found page.

We tell the router to render the result to the document body and dispatch a disconnect event to each page before leaving.

We have each page in a different file and we import them with the new dynamic import().


guard() is a function that given two functions, executes the first one if the user is authenticated, or the sencond one if not. It comes from auth.js so lets create a static/auth.js file with the following content:

export function isAuthenticated() {
    const token = localStorage.getItem('token')
    const expiresAtItem = localStorage.getItem('expires_at')
    if (token === null || expiresAtItem === null) {
        return false

    const expiresAt = new Date(expiresAtItem)
    if (isNaN(expiresAt.valueOf()) || expiresAt <= new Date()) {
        return false

    return true

export function guard(fn1, fn2) {
    return (...args) => isAuthenticated()
        ? fn1(...args)
        : fn2(...args)

export function getAuthUser() {
    if (!isAuthenticated()) {
        return null

    const authUser = localStorage.getItem('auth_user')
    if (authUser === null) {
        return null

    try {
        return JSON.parse(authUser)
    } catch (_) {
        return null

isAuthenticated() checks for token and expires_at from localStorage to tell if the user is authenticated. getAuthUser() gets the authenticated user from localStorage.

When we login, we’ll save all the data to localStorage so it will make sense.

Access Page

access page screenshot

Lets start with the access page. Create a file static/pages/access-page.js with the following content:

const template = document.createElement('template')
template.innerHTML = `
    <a href="/api/oauth/github" onclick="event.stopPropagation()">Access with GitHub</a>

export default function accessPage() {
    return template.content

Because the router intercepts all the link clicks to do its navigation, we must prevent the event propagation for this link in particular.

Clicking on that link will redirect us to the backend, then to GitHub, then to the backend and then to the frontend again; to the callback page.

Callback Page

Create the file static/pages/callback-page.js with the following content:

import http from '../http.js'
import { navigate } from '../router.js'

export default async function callbackPage() {
    const url = new URL(location.toString())
    const token = url.searchParams.get('token')
    const expiresAt = url.searchParams.get('expires_at')

    try {
        if (token === null || expiresAt === null) {
            throw new Error('Invalid URL')

        const authUser = await getAuthUser(token)

        localStorage.setItem('auth_user', JSON.stringify(authUser))
        localStorage.setItem('token', token)
        localStorage.setItem('expires_at', expiresAt)
    } catch (err) {
    } finally {
        navigate('/', true)

function getAuthUser(token) {
    return http.get('/api/auth_user', { authorization: `Bearer ${token}` })

The callback page doesn’t render anything. It’s an async function that does a GET request to /api/auth_user using the token from the URL query string and saves all the data to localStorage. Then it redirects to /.


There is an HTTP module. Create a static/http.js file with the following content:

import { isAuthenticated } from './auth.js'

async function handleResponse(res) {
    const body = await res.clone().json().catch(() => res.text())

    if (res.status === 401) {

    if (!res.ok) {
        const message = typeof body === 'object' && body !== null && 'message' in body
            ? body.message
            : typeof body === 'string' && body !== ''
                ? body
                : res.statusText
        throw Object.assign(new Error(message), {
            url: res.url,
            statusCode: res.status,
            statusText: res.statusText,
            headers: res.headers,

    return body

function getAuthHeader() {
    return isAuthenticated()
        ? { authorization: `Bearer ${localStorage.getItem('token')}` }
        : {}

export default {
    get(url, headers) {
        return fetch(url, {
            headers: Object.assign(getAuthHeader(), headers),

    post(url, body, headers) {
        const init = {
            method: 'POST',
            headers: getAuthHeader(),
        if (typeof body === 'object' && body !== null) {
            init.body = JSON.stringify(body)
            init.headers['content-type'] = 'application/json; charset=utf-8'
        Object.assign(init.headers, headers)
        return fetch(url, init).then(handleResponse)

    subscribe(url, callback) {
        const urlWithToken = new URL(url, location.origin)
        if (isAuthenticated()) {
            urlWithToken.searchParams.set('token', localStorage.getItem('token'))
        const eventSource = new EventSource(urlWithToken.toString())
        eventSource.onmessage = ev => {
            let data
            try {
                data = JSON.parse(
            } catch (err) {
                console.error('could not parse message data as JSON:', err)
        const unsubscribe = () => {
        return unsubscribe

This module is a wrapper around the fetch and EventSource APIs. The most important part is that it adds the JSON web token to the requests.

Home Page

home page screenshot

So, when the user login, the home page will be shown. Create a static/pages/home-page.js file with the following content:

import { getAuthUser } from '../auth.js'
import { avatar } from '../shared.js'

export default function homePage() {
    const authUser = getAuthUser()
    const template = document.createElement('template')
    template.innerHTML = `
            <button id="logout-button">Logout</button>
        <!-- conversation form here -->
        <!-- conversation list here -->
    const page = template.content
    page.getElementById('logout-button').onclick = onLogoutClick
    return page

function onLogoutClick() {

For this post, this is the only content we render on the home page. We show the current authenticated user and a logout button.

When the user clicks to logout, we clear all inside localStorage and do a reload of the page.


That avatar() function is to show the user’s avatar. Because it’s used in more than one place, I moved it to a shared.js file. Create the file static/shared.js with the following content:

export function avatar(user) {
    return user.avatarUrl === null
        ? `<figure class="avatar" data-initial="${user.username[0]}"></figure>`
        : `<img class="avatar" src="${user.avatarUrl}" alt="${user.username}'s avatar">`

We use a small figure with the user’s initial in case the avatar URL is null.

You can show the initial with a little of CSS using the attr() function.

.avatar[data-initial]::after {
    content: attr(data-initial);

Development Login

access page with login form screenshot

In the previous post we coded a login for development. Lets add a form for that in the access page. Go to static/pages/access-page.js and modify it a little.

import http from '../http.js'

const template = document.createElement('template')
template.innerHTML = `
    <form id="login-form">
        <input type="text" placeholder="Username" required>
    <a href="/api/oauth/github" onclick="event.stopPropagation()">Access with GitHub</a>

export default function accessPage() {
    const page = template.content.cloneNode(true)
    page.getElementById('login-form').onsubmit = onLoginSubmit
    return page

async function onLoginSubmit(ev) {

    const form = ev.currentTarget
    const input = form.querySelector('input')
    const submitButton = form.querySelector('button')

    input.disabled = true
    submitButton.disabled = true

    try {
        const payload = await login(input.value)
        input.value = ''

        localStorage.setItem('auth_user', JSON.stringify(payload.authUser))
        localStorage.setItem('token', payload.token)
        localStorage.setItem('expires_at', payload.expiresAt)

    } catch (err) {
        setTimeout(() => {
        }, 0)
    } finally {
        input.disabled = false
        submitButton.disabled = false

function login(username) {
    return'/api/login', { username })

I added a login form. When the user submits the form. It does a POST requets to /api/login with the username. Saves all the data to localStorage and reloads the page.

Remember to remove this form once you are done with the frontend.

That’s all for this post. In the next one, we’ll continue with the home page to add a form to start conversations and display a list with the latest ones.

Souce Code

Discuss on Twitter