If you want a scalable, high-performance backend with a fast-loading frontend, this guide is for you.
Let's introduce our technologies:
- Go is a backend language which has been tremendously popular since being launched by Google 10 years ago. It's simple to learn, has powerful concurrency primitives, and can deploy anywhere as a single binary.
- NextJS is a full-stack framework based on React, it gives you the benefits of react without the performance costs, since it lets you easily convert webpages to HTML and launch them around the world.
Thousands of major companies like Netflix and Notion use Go and NextJS in their stacks, and by the end of this guide you'll be able to as well.
Setup
Installing NodeJS and Go
You'll have to download and install two different tools before we can get started. They're available for all of the major operating systems.
Creating the project structure
Next, you'll have to create directories in the following structure:
We'll set up NextJS in the frontend - run their installation command to create an app there:
# create the NextJS base app in the frontend directory
user@computer:my-project/services$ npx create-next-app@latest
Need to install the following packages:
create-next-app@latest
Ok to proceed? (y)
✔ What is your project named? … frontend
✔ Would you like to use TypeScript with this project? … (No) / Yes
✔ Would you like to use ESLint with this project? … No / (Yes)
Creating a new Next.js app in /home/user/my-project/services/frontend.
# remove the .git directory and the pages/api directory from our frontend - we won't be using them.
user@computer:my-project/services$ rm -rf frontend/.git frontend/pages/api
Next, create a simple Go HTTP server at services/backend/main.go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
resp := []byte(`{"status": "ok"}`)
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))
}
Finally, we should be able to start both services (at different ports) by running two commands in two different terminals:
user@computer:my-project/services/frontend$ npm run dev
> [email protected] dev
> next dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
event - compiled client and server successfully in 1718 ms (161 modules)
user@computer:my-project/services/backend$ go run main.go
Server is available at http://localhost:8000
We can verify everything is set up correctly by visiting the two printed URLs from each step.
Finally (and optionally,) let's commit and push our changes to GitHub:
user@computer:my-project$ git init
Initialized empty Git repository in /home/user/my-project/.git/
user@computer:my-project$ git remote add origin [email protected]:My-Github-User/my-project
user@computer:my-project$ git add services
user@computer:my-project$ git commit -m 'First commit'
user@computer:my-project$ git push origin master
At this point, my repository looked like this.
Connecting your frontend with your backend
NextJS uses React.js to generate HTML. You can sort of think of it as a template that runs both on the frontend and the backend.
This means that we can fetch data from our backend, insert it into our frontend, and then send HTML all at once.
Edit the file at services/frontend/pages/index.js
and make its contents be the following;
import Head from 'next/head'
import styles from '../styles/Home.module.css'
export async function getServerSideProps() {
const res = await fetch("http://localhost:8000").then(x => x.json());
return {
props: {
status: res.status, //should be "ok"
}
}
}
export default function Home({status}) {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<div>Status is: {status}</div>
</main>
</div>
)
}
If you refresh your frontend, you'll notice that the {"status": "ok"}
we are sending from our backend gets pulled in via the getServerSideProps
call, and automatically templated into {status}
:
Let's add another route in our backend at services/backend/main.go
:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
var resp []byte
if req.URL.Path == "/status" {
resp = []byte(`{"status": "ok"}`)
} else if req.URL.Path == "/username" {
resp = []byte(`{"username": "colin"}`)
} 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))
}
Re-start the backend (going to the terminal running the backend, pressing control+c, and re-running go run main.go
)
Finally, modify our frontend at services/frontend/pages/index.js
to reference our second route:
export async function getServerSideProps() {
const {status} = await fetch("http://localhost:8000/status").then(x => x.json());
const {username} = await fetch("http://localhost:8000/username").then(x => x.json());
return {
props: {
status: status,
username: username,
}
}
}
export default function Home({status, username}) {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<div>Status is: {status}, your username is: {username}</div>
</main>
</div>
)
}
Finally, commit these changes as well:
user@computer:my-project$ git add services
user@computer:my-project$ git commit -m 'Add routes'
user@computer:my-project$ git push origin master
Next steps
- The biggest flaw with our current setup is that the backend requires an
if
statement for every single route. You should consider installing a Go web framework like Gin or Revel - Read the NextJS docs about Data fetching to better understand how
getServerSideProps
works - Add another page by reading the NextJS docs about pages