How to serve Vue SPA from Go (Golang)

A step-by-step information to constructing a Vue SPA with Go’s customary HTTP server

Photograph by Chris Ried on Unsplash

Full supply code will be discovered here.

Stipulations

  1. npm
  2. Vue CLI
  3. Go compile (1.16+ for embedding on this instance, advocate 1.18+)
  4. (Optionally available) Docker

The very very first thing is to arrange our venture. Let’s say our venture title is go_spa_example. Right here’s the code to try this:

mkdir go_spa_example
cd go_spa_example
# create Go module, I'm utilizing my Github right here
go mod init github.com/wolftsao/go_spa_example

Then initialize our Vue frontend:

# To make it easy, I simply title the Vue venture ui
# flag -n means no git
vue create -n ui

Now, Vue CLI will ask you concerning the venture choices. The Vue Router (since we’re constructing SPA) is crucial factor, so select “Manually choose options.” Listed here are my configurations:

preset: manually select features features needed: babel, router, linter Vue.js version: 3.x history mode: yes linter/formatter: basic additional lint features: lint on save prefer placing: in dedicated config files save as preset: n

Our venture setup is finished. Let’s deal with the frontend first.

We aren’t going to create fancy frontend issues right here, simply have a workable instance with default Vue template pages that can include the next pages:

  • Index web page with path "/"
  • About web page with path "/about"
  • All the things else will present the Not Discovered web page

We have to do the next to finish this:

  1. Change the placement of frontend construct artifacts
  2. Add a Not Discovered web page
  3. Setup alias for index web page and catch all routes for the Not Discovered web page

Change frontend artifacts location

If we construct the frontend now, the output artifacts will probably be in ui/dist, like this:

ui/dist
├── css
├── favicon.ico
├── index.html
└── js

The dist is now the foundation folder to serve the frontend, and it has every sort of static recordsdata, e.g., js, CSS, and so on., beneath its personal folder.

However this widespread structure is slightly bit difficult to deal with with Go’s customary HTTP library (I’ll clarify extra about this later).

Let’s mixture all of the static recordsdata right into a centralized location.

Open ui/vue.config.js then add the attribute assetsDir with the worth static like the next:

assetsDir in vue.config.js

The construction of artifacts now will probably be:

ui/dist
├── favicon.ico
├── index.html
└── static
├── css
└── js

All static recordsdata are beneath the static folder.

Primarily based on this configuration, the URL for our index web page will probably be:
http://<area>:<port>/

And CSS file will appear like this:
http://<area>:<port>/static/css/app.9bdcf330.css

Create Not Discovered web page

Create file ui/src/views/NotFound.vue with the next content material:

Vue Not Discovered template

Now, now we have a single file component (SFC) with a template solely.

Add Index web page alias and the catch all route

Lastly, open up ui/src/router/index.js, then:

  1. Add an alias alias: ['/index.htm'], to index web page route (line 7 under).
    By default, Vue deal with "/" and "/index.html" otherwise, so let’s make it extra pure by including this alias.
  2. Add a catch all route on the finish of routes array (line 20 under).

Right here’s the code to try this:

Catch all route in Vue Router

Wonderful! We’ve got accomplished our Vue frontend. Let’s embrace essentially the most thrilling half — the Go HTTP server.

This would be the important a part of this story. Nonetheless, to make issues so simple as attainable, I’ll outline the whole lot in a single important.go file.

Principally, what we want right here is:

  • A Go embedding bundle
  • Go HTTP server
  • Mux/router
  • HTTP file server to serve frontend static recordsdata

I may even speak about some errors when serving SPA from Go.

Embedding our frontend artifacts

We don’t really want to embed our frontend file, however embed has its personal potential utilization, so I take advantage of it right here as a apply.

(My venture supply code additionally has one other department, noembed, which makes use of a standard filesystem to serve the frontend. You’ll be able to test it out here)

The benefit of utilizing embed is that we are able to run our SPA wherever with out worrying concerning the location of frontend artifacts. Nonetheless, we have to rebuild our program each time there’s a frontend change.

OK. Let’s create the Go embed bundle file, ui/ui.go:

Go embed bundle

Be certain the file, ui.go, is beneath the ui folder, not the venture root. It lives with the opposite frontend recordsdata.

Observe: We will put ui.go within the venture root and alter bundle to important on this instance. However in an even bigger venture, we normally separate recordsdata into totally different packages, with every bundle serving its personal objective. The one objective for ui.go is to serve frontend artifacts. So, I made a decision to place it within the ui folder, nevertheless it’s simply my opinion.

This bundle declares a variable StaticFiles that serves because the embedding filesystem.

Additionally, take note of the Go directive //go:embed all:dist , which suggests embedding all recordsdata within the dist folder and subfolders.

In Go model sooner than 1.18, there’s a giant limitation within the embed, which can not embed recordsdata prefixed with "." or "_". And a few frontend bundle instruments generate artifacts with that sample.

Observe: This issue has been fastened in Go 1.18 with the particular time period all: within the embed directive. However since Vue CLI didn’t generate artifacts with "." or "_" prefix, we don’t have to fret about this (in case you are utilizing an earlier Go model, please change the directive to //go:embed dist/*, since all: is a 1.18 function.

So, should you prefer to attempt different frontend libraries/frameworks, or in case you are utilizing an earlier Go model, you have to pay extra consideration to those.

A easy Go HTTP server

Now, create the file important.go within the venture root folder to serve our HTTP server:

Go HTTP server important perform

You may want to regulate the import path for the ui bundle.

It’s only a regular Go HTTP server (and I hardcoded the server port to 8888).

Once more, to make the instance so simple as attainable, I put each route/handler in a single perform known as router. Let’s speak about this.

Our mux/router

The router perform will deal with three sorts of routes:

  1. Frontend index web page ("/" or index.html)
  2. Frontend static recordsdata (e.g., JavaScript, CSS, and so on.)
  3. APIs

Outline the router perform beneath important perform like this:

Go HTTP router
  • On line 5, we deal with the index web page with the perform indexHandler (I’ll work on this later)
  • On strains 8 to 10, we outline a http.FileServer by making a subfolder of our embedded filesystem. With that, we are able to search recordsdata beginning with "static" as an alternative of "dist" (By default, http.FileServer permits listing listings; you’ll be able to test this great article to disable that habits)
  • Line 13 is only a dummy API (nameless perform) for demo

Proper now, you may marvel why we separate index handler ("/") from the static file server ("/static/"). Isn’t index.html simply one other file?

Suppose we solely have the "/" route, which serves our file server if we go down the best path. We will full this by doing the next:

  • Construct and run this system
  • Sort http://localhost:8888 within the handle bar, and the index web page will present appropriately
  • Click on the About hyperlink to indicate the proper web page with the proper URL (http://localhost:8888/about)

All the things seems to be good till you sort http://localhost:8888/about straight within the handle bar, then hit enter. Increase! Empty web page with 404 web page not discovered.

What’s happening?

Nicely, the issue is twofold.

First, Go searches the filesystem by URL path. For the case of http://localhost:8888/about. That is telling Go to look the about file within the file server. Since we don’t have such a file, the Go HTTP server returns a default 404 message.

Second, the about web page solely exists within the frontend. JavaScript did the frontend routing magic for us. Technically, JavaScript/CSS code does exist in backend, so we want a file server to serve these recordsdata.

The frontend additionally wants an entry level to begin the SPA, very like Go’s important perform. When the browser accesses the index web page ("/", the entry level), it is aware of methods to fetch different static recordsdata, begin the Vue engine, after which Vue can route and render these pages for us.

That’s why we have to deal with index route "/" particularly.

There’s one last factor about Go’s HTTP route. Earlier, after we labored on Vue, we set assetsDir: 'static' within the vue.config.js, which is geared towards Go’s routing sample rule. (If you’re not acquainted with Go’s routing rule, I extremely advocate you to learn this. One of many biggest Go books I’ve ever learn!)

For Go, the whole lot else not matched by different handlers will fallback to index route /. That is the impact we wish, so we set it to our index web page. When the person varieties an arbitrary URL within the handle bar, it at all times falls again to index.html, then Vue can do the frontend routing appropriately.

Since "/" index route has been utilized by indexHandler, we have to use one other sample for the file server. But when we’re not setting assetsDir: 'static', we have to deal with every static file sort individually like this:

mux.Deal with("/css/", httpFS)
mux.Deal with("/js/", httpFS)

We’ve got spent fairly an enormous web page explaining these errors in Go. Let’s transfer on to our last piece of code, indexHandler.

Index web page handler

Outline indexHandler beneath router like this:

The code reads particular embedded recordsdata after which writes them to the HTTP response.

However it seems to be fairly complicated as a result of we additionally deal with the favicon right here.

Truthfully talking, I’m not fairly acquainted with the frontend. I didn’t discover a simple method to change the placement of the favicon file. The default location in Vue is beneath the foundation "/favicon.ico", so I dealt with it right here straight. (We will deal with it with a devoted handler like mux.Deal with("/favicon.ico", faviconHandler))

Now now we have accomplished our Go SPA. Time to roll!

Construct our frontend first, so there’s artifacts for Go to embed. Here is code:

cd ui
npm run construct

Run our Go HTTP server:

cd ..
go run important.go

Let’s make some checks. Open http://localhost:8888 (we hardcoded the port to 8888), and your browser ought to present default to the Vue residence web page:

Vue default residence web page

Click on the highest “About” hyperlink and alter the about web page:

Vue default about web page

Sort http://localhost:8888/about within the handle bar, then hit enter. It ought to present the identical about web page.

Sort an arbitrary URL like http://localhost:8888/login to indicate the not discovered web page:

Vue not discovered web page

Superior! All the things seems to be nice. Let’s use CURL to check our dummy API utilizing this command:

curl -i "http://localhost:8888/api/v1/greeting"

Output:

HTTP/1.1 200 OK
Date: Fri, 20 Might 2022 02:47:58 GMT
Content material-Size: 13
Content material-Sort: textual content/plain; charset=utf-8
Hi there, there!

Good, let’s check extra. How a few nonexistent API, which may occur if a person typed the flawed handle.

curl -i "http://localhost:8888/api/v1/greting"

Output:

HTTP/1.1 200 OK
Date: Fri, 20 Might 2022 02:53:18 GMT
Content material-Size: 611
Content material-Sort: textual content/html; charset=utf-8
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Suitable" content material="IE=edge"><meta title="viewport" content material="width=device-width,initial-scale=1"><hyperlink rel="icon" href="/favicon.ico"><title>ui</title><script defer="defer" src="/static/js/chunk-vendors.ea2100d5.js"></script><script defer="defer" src="/static/js/app.fc9d9338.js"></script><hyperlink href="/static/css/app.9bdcf330.css" rel="stylesheet"></head><physique><noscript><sturdy>We're sorry however ui does not work correctly with out JavaScript enabled. Please allow it to proceed.</sturdy></noscript><div id="app"></div></physique></html>

Oops… How come the nonexistent API name labored?

In the event you look carefully, the response physique is definitely index.html of our frontend.

The issue is, we don’t have any route that matches that URL, so it falls again to the index route "/".

Let’s add some guard clauses to stop this:

Go HTTP index handler
  • On line 2, we add a technique test to solely enable the GET methodology
  • On line 8, if URL.Path is prefixed with /api, we return the not discovered response

(Ideally, in a well-organized venture, it’s higher to place these circumstances in issues like middleware)

Let’s rebuild our program and check out the nonexistent API once more. It ought to return 404 this time:

HTTP/1.1 404 Not Discovered
Content material-Sort: textual content/plain; charset=utf-8
X-Content material-Sort-Choices: nosniff
Date: Fri, 20 Might 2022 03:14:42 GMT
Content material-Size: 19
404 web page not discovered

Congrats! We actually have come a good distance and constructed our full-fledged single-page software with Go efficiently.

We will additionally construct our software in Docker.

Create a dockerfile in our venture root with the next content material:

Go SPA dockerfile

Then:

# 1. construct frontend
cd ui
npm run construct
# 2. construct docker picture
cd ..
docker construct -t go-spa:check .
# 3. (non-compulsory) take away intermediate pictures
docker picture prune -f
# 4. run our SPA container
docker run --rm -p "8888:8888" go-spa:check

It is a lengthy story, nevertheless it’s fairly quick and easy should you have a look at the total Go code. We have to perceive the mechanisms and limitations of Go’s HTTP server and make some lodging.

The ideas are:

  1. perceive the construction of frontend construct artifacts, discover and modify attainable configuration settings, then serve these recordsdata in Go appropriately.
  2. perceive the pattern-matching guidelines and quirks of Go’s HTTP handler

Earlier than I began penning this story, I did spend plenty of time determining methods to make this work. I’m not an skilled programmer, particularly in frontend, so please let me know if there are higher options or if one thing is inaccurate right here.

Thanks quite a bit for studying!

More Posts