Logo credits to vuejs.org and golang.org

Social application with Vue.js and GO

Create and serve a twitter like application with vue.js and golang PART 7: Connection with GOLANG server

Ivano Dalmasso
Published in
8 min readMar 22, 2021

--

This is the seventh part of this serie. Check here all the parts:

In this lesson our simple application is starting to finally talk with our golang backend server, we will start writing logic on both frontend for communication and backend to data storage the posts data.

We will update both the frontend and the backend in this session, you can find the code here:

Create logic for posts on backend

Let’s begin creating some logic on the backend enabling us to manipulate posts. Create a folder “<backend_source_folder>/endpoints” (that actually will translate in creating a package with that same name) and inside it create two files: utils.go and posts.go.

In the utils.go file insert the following code:

package endpoints;import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
)
//AddRouterEndpoints add the actual endpoints for api
func AddRouterEndpoints(r *mux.Router) *mux.Router {
r.HandleFunc("/api/posts", getPosts).Methods("GET")
r.HandleFunc("/api/posts", addPost).Methods("POST")
r.HandleFunc("/api/posts/{POST_ID}",deletePost)
.Methods("DELETE")
r.HandleFunc("/api/posts/{POST_ID}/comments",addComment)
.Methods("POST")
return r
}

func sendJSONResponse(w http.ResponseWriter, data interface{}) {
body, err := json.Marshal(data)
if err != nil {
log.Printf("Failed to encode a JSON response: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
_, err = w.Write(body)
if err != nil {
log.Printf("Failed to write the response body: %v", err)
return
}
}

These functions are pretty easy to understand: the first is going to update a router that comes as parameter adding the route resolve methods for adding, getting and deleting posts. As addition, we also add a method to insert a comment to it. We are using gorilla mux, that is actually a multiplexer that makes more easy and organised the code that manages the routes.

The code of the first function actually just says that if the /api/posts endpoint of our server is called with an http GET method, it should be server with the getPost function (that we haven’t written yet),and following the other ones.

The second function is going to be an helper function that we’ll use in the routes functions. This is going to take as input an http.ResponseWriter and an empty interface (that could be, as always, almost anything in go) and then it write with the response writer the object serialized as json with an http OK code. Note that if the object is not json-serializable it will return an error.

With this setup, we are going to insert in the second file the actual functions that have to handle the routes. At the top of the file we insert

package endpoints

import (
"encoding/json"
"log"
"net/http"
"strconv"
"time"

"github.com/gorilla/mux"
)
type comment struct {
ID int `json:"id"`
Username string `json:"username"`
Post string `json:"post"`
Date time.Time `json:"date"`
}
//post Struct is used as post structure...
type post struct {
ID int `json:"id"`
Username string `json:"username"`
Post string `json:"post"`
Date time.Time `json:"date"`
Comments []comment `json:"comments"`
}

This is the definition of two structs, one with name comment and the other with name post, containing the fields we need for the data transmission of the APIs. Note that the field names are all capitalized, that is needed for the json-serialization. We are not going to use a database right by now, we’ll add it in a future lesson, now we’ll just create an array of posts and use them in memory:

var posts []post=make([]post, 0)
//need an index for the array... When I'll delete the posts the index will have to go on...
var index int=1

After this we can start creating the actual handler functions:

//addPost will get in Body a post with ONLY username and post-> need to add the others and save it
func addPost(w http.ResponseWriter, r *http.Request) {
log.Println("addPost called")
var actualPost post
err:=json.NewDecoder(r.Body).Decode(&actualPost)
if err!=nil{
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
actualPost.ID = index
index++
actualPost.Date=time.Now()
if actualPost.Comments== nil{
actualPost.Comments=make([]comment, 0)
}
posts=append(posts, actualPost)
sendJSONResponse(w,actualPost)
}
//deletePost removes the post that is being passed. Get the id from //the query
func deletePost(w http.ResponseWriter, r *http.Request) {
log.Println("deletePost called")
vars := mux.Vars(r)
idString, ok := vars["POST_ID"]
if !ok {
http.Error(w, "Cannot find ID", http.StatusBadRequest)
return
}
id, err := strconv.Atoi(idString)
if err!=nil{
http.Error(w, "Cannot convert the id value to string",http.StatusBadRequest)
return
}
for i:=0;i<len(posts);i++{
if posts[i].ID==id {
posts[i]=posts[len(posts)-1]
posts=posts[:len(posts)-1]
w.WriteHeader(http.StatusOK)
return
}
}
http.Error(w, "Cannot find the requested id", http.StatusNotFound)
}
//addComment will get the comment in the body, and the id in the query
func addComment(w http.ResponseWriter, r *http.Request) {
log.Println("addComment called")
vars := mux.Vars(r)
idString, ok := vars["POST_ID"]
if !ok {
http.Error(w, "Cannot find ID", http.StatusBadRequest)
return
}
id, err := strconv.Atoi(idString)
if err!=nil{
http.Error(w, "Cannot convert the id value to string", http.StatusBadRequest)
return
}
var actualComment comment
err = json.NewDecoder(r.Body).Decode(&actualComment)
if err!=nil{
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
for i:=0;i<len(posts);i++{
if posts[i].ID==id {
//Now I have the post
var commMax int=0
for comm:=0;comm<len(posts[i].Comments);comm++{
if commMax<posts[i].Comments[comm].ID{
commMax=posts[i].Comments[comm].ID
}
}
actualComment.ID=commMax+1
actualComment.Date=time.Now()
posts[i].Comments = append(posts[i].Comments, actualComment)
sendJSONResponse(w, posts[i])
return
}
}
//If I'm here, there is no post with the id searched...
http.Error(w, "Cannot find a post with the selected id", http.StatusNotFound)
}
//getPosts will return all the posts actually in the array
func getPosts(w http.ResponseWriter, r *http.Request) {
log.Println("Get post called")
sendJSONResponse(w, posts)
}

Let’s explain this code:

  • The addPost function actually just try to decode the request body as a Post object. If this can be done, it adds this object, with a populated id and date, to our posts “storage”, and then returns it back, updated, with an ok response
  • The deletePost function gets the POST_ID query parameter, and if there is a post in the “storage” with that id, it gets deleted. Else, it returns a NotFound status code to the client.
  • The addComment adds a comment object passed (if decoded successfully) to the post indicated by the POST_ID query parameter. Actually the code for looking up the ID is not nice at all, but as soon as we’ll start using a database we’ll replace it.
  • The getPosts just write as answer the list of posts serialized as json.

Now all objects can be called, the last thing to do is to call in the main function of the server the addRouterEndpoint method wrote before in the utils file. Then, to manage the CORS policies, we just write a simple decorator that will be applied to the router before passing it to the http.Handle function. This is simply done like this:

func main(){
r := mux.NewRouter()
r=endpoints.AddRouterEndpoints(r)
fs := http.FileServer(http.Dir("./dist"))
r.PathPrefix("/").Handler(fs)

http.Handle("/",&corsRouterDecorator{r})
fmt.Println("Listening")
log.Panic(
http.ListenAndServe(":3000", nil),
)
}


type corsRouterDecorator struct {
R *mux.Router
}

func (c *corsRouterDecorator) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if origin := req.Header.Get("Origin"); origin != "" {
rw.Header().Set("Access-Control-Allow-Origin", origin)
rw.Header().Set("Access-Control-Allow-Methods",
"POST, GET, PUT, DELETE, PATCH")
rw.Header().Add("Access-Control-Allow-Headers",
"Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With")
}
// Stop here if its Preflighted OPTIONS request
if req.Method == "OPTIONS" {
return
}
c.R.ServeHTTP(rw, req)
}

That’s it! with this, we have the 4 wanted routes created and served by this server, that will then manipulate the posts and comment objects. Note that in this chapter we’re not going to manage the authentication, so any user that will send a post to the /api/posts POST route will be able to add a new post to our server. We will do that in another chapter.

Updating the frontend

In the frontend most of the work is going to be done in the store actions. Note that we changed in backend the “post.user” to “post.username”, so just for semplicity let’s change it anywhere.

Then, in the index.js of the Post store, let’s make some modification. As first, we won’t have anymore an “ADD_COMMENT” mutation but, because the route returns us the complete post object, we can instead have a “SET_POST_COMMENTS” mutation like this:

SET_POST_COMMENTS(state, { postId, post }) {
const oldPost = state.posts.find(post => post.id == postId);
oldPost.comments = post.comments;
}

then, the ADD_POST mutation doesn’t need to find an id anymore, so it can become more simple. Also, let’s add a SET_ALL_POSTS mutation for initialization:

ADD_POST(state, post) {
state.posts.push(post);
},
SET_ALL_POSTS(state, posts) {
state.posts = posts;
},

WIth this we have changed and simplified our mutations. We can now update the actions to call the api too and add the new action “getAllPosts” to complete the functionalities we need. This is done with the following code:

async addPost(context, post) {
fetch("http://localhost:3000/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(post)
})
.then(response => {
if (!response.ok) {
throw Error(response.body);
}
return response.json();
})
.then(data => {
context.commit("ADD_POST", data);
})
.catch(error => {
console.log(error);
});
},
async deletePost(context, { post }) {
fetch("http://localhost:3000/api/posts/" + post.id, {
method: "DELETE"
})
.then(response => {
if (response.ok) {
console.log(response);
context.commit("DELETE_POST", post.id);
return;
}
throw Error(response);
})
.catch(error => {
console.log(error);
});
},
async addComment(context, { postId, comment }) {
fetch("http://localhost:3000/api/posts/" + postId + "/comments", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(comment)
})
.then(response => {
if (response.ok) {
return response.json();
} else throw Error(response.body);
})
.then(data => {
context.commit("SET_POST_COMMENTS", { postId: postId, post: data });
})
.catch(error => {
console.log(error);
});
},
async getAllPosts(context) {
fetch("http://localhost:3000/api/posts")
.then(response => {
if (response.ok) {
return response.json();
} else {
throw Error(response.body);
}
})
.then(data => {
console.log(data);
context.commit("SET_ALL_POSTS", data);
})
.catch(error => {
console.log(error);
});
}

So, all these actions actually calls with a fetch the new relative api we created on the server. In the resolution of the returned promises, if the response is ok, we call the commit of the mutation, else we just console.log the error. Obviously when the promise resolution has an object parameter that we must later use, we have to deserialize it, and this is done really easily with the promise resolution of the response.json() method. With that we could then also send the data to the mutations we have to commit.

Note that we have never added a delete button for the posts, so let’s just do it now: in the SinglePost component, add in the slot header of the card a normal button like

<button class="delete-button" @click.prevent="deletePost">        Delete
</button>

And the logic behind it is pretty easy, we must dispatch the detetePost action with the post value:

deletePost() {
this.$store.dispatch("posts/deletePost", { post: this.post }); }

Now we need only to initialize the Posts store.

We can do it in the Posts.vue component, using the mounted() configuration method like this:

mounted() {    
this.$store.dispatch("posts/getAllPosts");
}

Like this,when the posts component is mounted, it calls the getAllPosts action of the store, that fills the store with all the posts from the server. Then, every action in the vue user interface calls the relative action of the store, and the update of the store automatically is reflected in the components.

The fact that the actions are async, permits to do some action when they resolve (so, for example, we could set a waiting gif or animation when the user click on delete post, and remove the animation when the action resolve).

With this setup, we can then launch the server, and after the vue client, and then everything should work fine, with the posts that are actually added to the backend storage.

We have actually still two big problems to solve:

  1. The posts are actually maintained in memory, this will be solved in next lessons, right by now if we stop the server the posts are actually lost forever, but at least the client is now persistent, and also reopening the page will show again the data.
  2. The authentication, now any user can store a new post, by passing a post object to the server.

With the next chapter we are going to address this last problem, so only an authenticated user will be able to login and use the actual application.

--

--

Ivano Dalmasso

Always looking to learn new things, and loving see things work as I want