Using WebSockets with NextJS and Golang
Reading Time • 5 min read
Using WebSockets with NextJS and Golang
Using WebSockets with NextJS and Golang

In our last post, we set up a simple full-stack project with NextJS and Go.

Collaborative webapps (aka real-time apps)

Most apps use CRUD (create / read / update /delete) APIs - one where the frontend sends changes to the backend, but not vice-versa.

That topic has been extensively covered by some excellent existing tutorials, so let's do something different: An app like Google Docs where multiple users can read/write a document concurrently and see other users' changes reflected in real time.

This sort of app doesn't have a standard name, though you might've heard of  a "real-time app," or frameworks like Phoenix LiveView for Elixir. It's easiest to explain the difference between CRUD and these real-time apps with two diagrams.

CRUD apps allow you to send changes to a server, and allow other users to request those changes. For example, when you make a new post on Instagram, other users can't see your post until they refresh the page:

A traditional "CRUD" app

In a real-time app, all clients keep persistent WebSocket connections to the backend, and receive updates as they happen, instead of needing to wait for the user to refresh the page:

A real-time app like Figma or Google Docs

Once we're done this tutorial and the next few, we'll be able to build something that looks like Google Docs:

Enough theory, let's get coding

Let's update our sample app so that we use a WebSocket to update the state of the page after the initial page load.

The architecture

This architecture means that you can immediately view the contents of the webpage before any websockets are made (server-side rendering with NextJS), while staying updated for changes by connecting directly to the Go server.

It also means that we can keep all of our actual code in Go, and avoid using Javascript or Typescript for our backend, while still being able to use React.

Let's start with the sample project from the initial setup post: git clone https://github.com/webappio/golang-nextjs-example.git

Rename the endpoints

We'll start by renaming the endpoints - open main.go and change the handler:

func main() {
	handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		var resp []byte
		if req.URL.Path == "/handler-initial-data" {
			resp = []byte(`{"text": "initial"}`)
		} else if req.URL.Path == "/handler" {
			time.Sleep(time.Second) //TODO HACK: sleep a second to check everything is working properly

			resp = []byte(`{"text": "updated"}`)
            
			//this line is required because the browser will check permissions to avoid getting hacked
			rw.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
		} else {
			rw.WriteHeader(http.StatusNotFound)
			return
		}

		rw.Header().Set("Content-Type", "application/json")
		rw.Header().Set("Content-Length", fmt.Sprint(len(resp)))
		rw.Write(resp)
	})

	log.Println("Server is available at http://localhost:8000")
	log.Fatal(http.ListenAndServe(":8000", handler))
}

Now we have two endpoints, http://localhost:8000/handler-initial-data is what NextJS will initially send to the browser (as HTML, due to server-side rendering), and then http://localhost:8000/handler is what our app will pull its data from.

Adding an asynchronous update to the frontend

Next, update the frontend to reference our /handler-initial-data and /handler paths in index.js:

import Head from 'next/head'
import styles from '../styles/Home.module.css'
import {useEffect, useState} from "react";

export async function getServerSideProps() {
    const initialData = await fetch("http://localhost:8000/handler-initial-data").then(x => x.json());
    return {props: {data: initialData}}
}

export default function Home(props) {
    const [data, setData] = useState(props.data);
    useEffect(() => {
        fetch("http://localhost:8000/handler")
            .then(x => x.json())
            .then(x => setData(x));
    }, [])
    return (
        <div className={styles.container}>
            <Head>
                <title>OSS Docs</title>
                <meta name="description" content="Fast like SSR, Powerful like WebSockets"/>
                <link rel="icon" href="/favicon.ico"/>
            </Head>

            <main className={styles.main}>
                <h1 className={styles.title}>
                    {props.title || "Untitled Document"}
                </h1>
                <div>Data is: {JSON.stringify(data)}</div>
            </main>
        </div>
    )
}

If you run the backend with go run  main.go and the frontend with npm run dev, you should initially see Data is: {"text":"initial"} and then a second later see Data is: {"text":"updated"} - the operations that occur are:

  1. Your browser asks NextJS for the website HTML
  2. NextJS calls getServerSideProps() and function Home(props) to generate that HTML, causing a HTTP request to http://localhost:8000/handler-initial-data
  3. Your browser receives the HTML and JavaScript, and runs function Home locally
  4. A second later, the useEffect hook finishes getting its data from /handler in your browser, and updates data, which then changes your site locally

Adding a websocket

To build something like Google Docs, we'd like to be able to send our changes to the backend and also receive other peoples' changes as they come in. That means we need bi-directional communication, which is exactly what WebSockets help with.

Go doesn't come with a WebSocket library, so let's install one:

# NOTE: if you have a GitHub or GitLab repository, change this line 
user@computer:my-project/services/backend$ go mod init github.com/webappio/golang-nextjs-example

user@computer:my-project/services/backend$ go get github.com/gobwas/ws

And update our backend to handle WebSockets at /handler:

if req.URL.Path == "/handler" {
	conn, _, _, err := ws.UpgradeHTTP(req, rw)
	if err != nil {
		log.Println("Error with WebSocket: ", err)
		rw.WriteHeader(http.StatusMethodNotAllowed)
		return
	}
	go func() {
		defer conn.Close()

		time.Sleep(time.Second) //TODO HACK: sleep a second to check everything is working properly
		err = wsutil.WriteServerMessage(conn, ws.OpText, []byte(`{"text": "from-websocket"}`))
		if err != nil {
			log.Println("Error writing WebSocket data: ", err)
			return
		}
	}()
	return
}

Adding the installed library to our imports:

import (
	"fmt"
	"github.com/gobwas/ws"
	"github.com/gobwas/ws/wsutil"
	"log"
	"net/http"
	"time"
)

Finally, we can change our frontend to use a WebSocket instead of a fetch call:

export default function Home(props) {
    const [data, setData] = useState(props.data);
    const [ws, setWS] = useState(null);
    useEffect(() => {
        const newWS = new WebSocket("ws://localhost:8000/handler")
        newWS.onerror = err => console.error(err);
        newWS.onopen = () => setWS(newWS);
        newWS.onmessage = msg => setData(JSON.parse(msg.data));
    }, [])
    return (
        <div className={styles.container}>
            <Head>
                <title>OSS Docs</title>
                <meta name="description" content="Fast like SSR, Powerful like WebSockets"/>
                <link rel="icon" href="/favicon.ico"/>
            </Head>

            <main className={styles.main}>
                <h1 className={styles.title}>
                    {props.title || "Untitled Document"}
                </h1>
                <div>Data is: {JSON.stringify(data)}</div>
            </main>
        </div>
    )
}

After restarting the backend (ctrl + c, up arrow, enter), you should see the text quickly change from Data is: {"text":"initial"} to Data is: {"text":"from-websocket"} - the same result as our fetch implementation, but now we have two new methods:

  • ws.send(...) in our frontend will send data to our backend, and
  • wsutil.WriteServerMessage in our backend will send data to our frontend

The repository for this article (as it was at the time of writing) is available here.

Check back next week to see how we can use this foundation to make a performant clone of Google Docs.

Last Updated • Dec 19, 2022