Goal
Deploy a Vue.js Single Page Application (SPA) with a Golang API backend on Platform.sh
Assumptions
- an empty Platform.sh project with Medium plan
- Node.js installed locally
- Vue.js CLI installed locally (
npm install -g @vue/cli
): https://cli.vuejs.org/
Problems
Two strategies are possible when building an SPA with Platform.sh:
- creating two separate projects for Vue.js frontend and Golang API backend
- hosting both apps within a single multi-app Platform project (requires at least a Medium plan)
This How-to shows the second option.
Steps (Multi-app SPA)
1. Project structure
Ultimately, the project structure will look like the following (one common .platform
directory, and one .platform.app.yaml
file per app):
vuespa/
.git/
.platform/
routes.yaml
services.yaml
hello_world_backend/
.platform.app.yaml
hello_world.go
hello_world_frontend/
.platform.app.yaml
node_modules/
public/
src/
App.vue
main.js
babel.config.js
package-lock.json
package.json
2. Set Up Golang App
1. Create the Go Project
Create and enter a project directory vuespa
. Create a hello_world_backend
directory, and then create the following hello_world.go
file within that directory:
// vuespa/hello_world_backend/hello_world.go
package main
import (
"encoding/json"
"fmt"
"net/http"
psh "github.com/platformsh/gohelper"
"github.com/rs/cors"
)
// Greetings is a basic Greetings to user
type Greetings struct {
Message string `json:"message"`
}
// sayHello returns greetings to user in JSON format
func sayHello(w http.ResponseWriter, r *http.Request) {
greetings := Greetings{Message: "Hello World!"}
returnedJSON, err := json.Marshal(greetings)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, "%s", returnedJSON)
}
func main() {
// Load Platform.sh environment variables in order to retrieve correct port
p, err := psh.NewPlatformInfo()
if err != nil {
panic("Not in a Platform.sh Environment.")
}
// Initialize HTTP server
mux := http.NewServeMux()
// Set up the /say-hello API endpoint
mux.HandleFunc("/say-hello", sayHello)
// Enable CORS and Whitelist the frontend domain in order to comply with
// CORS Allowed Origins policy
handler := cors.New(cors.Options{
AllowedOrigins: []string{"https://your-platformsh-frontend-url"},
}).Handler(mux)
// Launch HTTP server with custom port retrieved in Platform.sh env var
http.ListenAndServe(":"+p.Port, handler)
}
The above exposes a say-hello
REST endpoint returning a greetings message to user.
CORS need to be properly handled as both frontend and backend apps are not hosted at the same URLs. Ideally the frontend URL whitelisted in CORS (https://your-platformsh-frontend-url
) should be retrieved through an environment variable.
For more information about CORS please visit: https://wikipedia.org/wiki/Cross-origin_resource_sharing
The frontend url will not be made available until the project is pushed to Platform.sh for the first time, so it may be necessary to push the code once to establish those routes, and then commit the url once they are defined.
In general, they will take the forms https://https://master-7rqtwti-<project ID>.<project region>.platformsh.site
for the frontend url and https://https://backend.master-7rqtwti-<project ID>.<project region>.platformsh.site
for the backend url.
2. Set up the Platform.sh configuration
Create a .platform.app.yaml
within the directory:
# vuespa/hello_world_backend/.platform.app.yaml
name: go-backend
# Use the Golang 1.12 image
type: golang:1.12
# A Medium plan is necessary for multi-app
size: M
hooks:
# Get dependencies and build Go app
build: |
go get ./...
go build -o bin/app
web:
upstream:
socket_family: tcp
protocol: http
# Launch the Go server
commands:
start: ./bin/app
locations:
/:
allow: false
passthru: true
disk: 1024
3. Set Up Vue.js App
1. Initialize the Vue.js project
Create the Vue.js base project with the Vue CLI, selecting the default
install option when prompted:
$ vue create hello_world_frontend
cd
into hello_world_frontend
and install the axios
dependency:
$ npm install axios
2. Update The Vue.js Project
Remove everything inside the src
directory except the main.js
file and add the following App.vue
file:
<!-- vuespa/hello_world_frontend/src/App.vue -->
<template>
<div id="app">
<p v-if="msg">
Retrieved the following greetings message from API Go backend:
<b>{{ msg }}</b>
</p>
<p v-if="msgError">
Error while getting message from Go API backend:
<b>{{ msgError }}</b>
</p>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'app',
data() {
return {
msg: '',
msgError: '',
}
},
mounted: function () {
axios.get('https://your-platformsh-backend-url/say-hello')
.then(response => {
if (response.status === 200) {
if (response.data) {
this.msg = response.data.message
} else {
this.msgError = 'Got no data from API'
}
} else {
this.msgError = 'Got an ok but unhandled HTTP response from API'
}
})
.catch(e => {
if (e.response) {
if (e.response.status === 500) {
this.msgError = 'Got an internal server error from API'
} else {
this.msgError = 'Got an unhandled error HTTP response from API'
}
} else if (e.request) {
this.msgError = 'Got no response when contacting API server'
} else {
this.msgError = 'Got an unhandled error when contacting API server'
}
})
}
}
</script>
3. Set up the Platform.sh configuration
Now create a .platform.app.yaml
with the following:
# vuespa/hello_world_frontend/.platform.app.yaml
name: vuejs-frontend
type: nodejs:10
# A Medium plan is necessary for multi-app
size: M
hooks:
build: |
npm install
npm run build
# There is no need for a writable file mount, so set it to the smallest possible size.
disk: 256
web:
commands:
# Run a no-op process that uses no CPU resources, since this is a static site.
start: sleep infinity
locations:
"/":
root: "dist"
index:
- "index.html"
# Set a 5 minute cache lifetime on all static files.
expires: 300s
# Disable all scripts, since we don't have any anyway.
scripts: false
# By default do not allow any files to be served.
allow: false
# Whitelist common image file formats, plus HTML files, robots.txt
# All other requests will be rejected.
rules:
\.(css|js|gif|jpe?g|png|ttf|eot|woff2?|otf|html|ico|svg?)$:
allow: true
^/robots\.txt$:
allow: true
4. Set Up Routes and Services
In the root directory create a .platform
directory. Create a routes.yaml
file.
# vuespa/.platform/routes.yaml
"https://backend.{default}/":
type: upstream
upstream: "go-backend:http"
"https://{default}/":
type: upstream
upstream: "vuejs-frontend:http"
Create an empty services.yaml
file.
# vuespa/.platform/services.yaml
5. Deploy
Set the repository’s remote to an empty Platform.sh project using its project ID
, which can be found within the Web UI or by listing the accounts active projects with the Platform.sh CLI, platform project:list
.
$ platform project:set-remote <project ID>
Commit changes and deploy.
$ git init
$ git add .
$ git commit -m "Init commit"
$ git push platform master
6. Verify
Visit the frontend URL to see the following message:
Retrieved the following greetings message from API Go backend: Hello World!
Conclusion
Setting Up a Vue.js SPA + Golang API within a muti-app project on Platform.sh is pretty simple. It requires no local compilation and the project is instantly testable via an HTTPS URL.
An improvement would be to dynamically retrieve the backend URL in the frontend app (for API results fetching), and conversely to retrieve the frontend URL in the backend app (for CORS whitelisting).