Build a photo feed with Go and Vue.js

Introduction

Many social media based applications allow users to upload photos and these photos are usually displayed in a timeline for their followers and others to see. In the past, you would have had to refresh your feed manually to see new photos uploaded to the timeline. However, with modern web technologies, you can see the updates in realtime without having to refresh the page manually.

In this article, we will consider how you can build a realtime photo feed using Pusher Channels, GO and a little Vue.js. Pusher Channels helps you “easily build scalable in-app notifications, chat, realtime graphs, geotracking and more in your web & mobile apps with our hosted pub/sub messaging API.”

This is a preview of what we will be building:

go-photo-feed-demo

Prerequisites

Before we start building our application, make sure you have:

  • Basic knowledge of the Go programming language.
  • Basic JavaScript (Vue.js) knowledge.
  • Go (version >= 0.10.x) installed on your machine. Check out the installation guide.
  • SQLite (version >= 3.x) installed on your machine.

Let’s get started.

Getting a Pusher Channels application

The first step will be to get a Pusher Channels application. We will need the application credentials for our realtime features to work.

Go to the Pusher website and create an account. After creating an account, you should create a new application. Follow the application creation wizard and then you should be given your application credentials, we will use this later in the article.

go-photo-feed-app-keys

Now that we have our application, let’s move on to the next step

Creating our Go application

The next thing we want to do is create the Go application. In your terminal, cd to your $GOPATH and create a new directory there.

1$ cd $GOPATH/src
2    $ mkdir gofoto
3    $ cd gofoto

💡 It is recommended that you place the source code for your project in the src subdirectory (e.g., $GOPATH/src/your_project or $GOPATH/src/github.com/your_github_username/your_project.

Next, we will create some directories to organize our application a little:

1$ mkdir database
2    $ mkdir public
3    $ mkdir public/uploads

This will create a database and public directory, and also an uploads directory inside the public directory. We will store our database file inside the database directory, we will keep our public files: HTML and images, inside the public and uploads directory. Create a new index.html file in the public directory that was created.

Now let’s create our first (and only) Go file for this article. We will try to keep everything simple by placing all our source code in a single file. Create a main.go file in the project root.

In the file paste the following:

1package main
2    
3    import (
4        "database/sql"
5        "io"
6        "net/http"
7        "os"
8        
9        "github.com/labstack/echo"
10        "github.com/labstack/echo/middleware"
11        _ "github.com/mattn/go-sqlite3"
12        pusher "github.com/pusher/pusher-http-go"
13    )

Above we have imported some packages we will be needing to work on our photo feed. We need the database/sql to run SQL queries, the io and os package for our file uploading process, and the net/http for our HTTP status codes.

We have some other external packages we imported. The labstack/echo package is the Echo framework that we will be using. We also have the mattn/go-sqlite3 package which is for SQLite. Finally, we imported the pusher/pusher-http-go package which we will use to trigger events to Pusher Channels.

Importing external Go packages

Before we continue, let’s pull in these packages using our terminal. Run the following commands below to pull the packages in:

1$ go get github.com/labstack/echo
2    $ go get github.com/labstack/echo/middleware
3    $ go get github.com/mattn/go-sqlite3
4    $ go get github.com/pusher/pusher-http-go

Note that the commands above will not return any confirmation output when it finishes installing the packages. If you want to confirm the packages were indeed installed you can just check the $GOPATH/src/github.com directory.

Now that we have pulled in our packages, let’s create the main function. This is the function that will be the entry point of our application. In this function, we will set up our applications database, middleware, and routes.

Open the main,go file and paste the following code:

1func main() {
2        db := initialiseDatabase("database/database.sqlite")
3        migrateDatabase(db)
4        
5        e := echo.New()
6    
7        e.Use(middleware.Logger())
8        e.Use(middleware.Recover())
9    
10        e.File("/", "public/index.html")
11        e.GET("/photos", getPhotos(db))
12        e.POST("/photos", uploadPhoto(db))
13        e.Static("/uploads", "public/uploads")
14    
15        e.Logger.Fatal(e.Start(":9000"))
16    }

In the code above, we instantiated our database using the file path to the database file. This will create the SQLite file if it did not already exist. We then run the migrateDatabase function which migrates the database.

Next, we instantiate Echo and then register some middlewares. The logger middleware is helpful for logging information about the HTTP request while the recover middleware “recovers from panics anywhere in the chain, prints stack trace and handles the control to the centralized HTTPErrorHandler.”

We then set up some routes to handle our requests. The first handler is the File handler. We use this to serve the index.html file. This will be the entry point to the application from the frontend. We also have the /photos route which accepts a POST and GET request. We need these routes to act like API endpoints that are used for uploading and displaying the photos. The final handler is Static. We use this to return static files that are stored in the /uploads directory.

We finally use e.Start to start our Go web server running on port 9000. The port is not set in stone and you can choose any available and unused port you feel like.

At this point, we have not created most of the functions we referenced in the main function so let’s do so now.

Creating our database management functions

In the main function we referenced an initialiseDatabase and migrateDatabase function. Let’s create them now. In the main.go file, paste the following functions above the main function:

1func initialiseDatabase(filepath string) *sql.DB {
2        db, err := sql.Open("sqlite3", filepath)
3        if err != nil || db == nil {
4            panic("Error connecting to database")
5        }
6     
7        return db
8    }
9    
10    func migrateDatabase(db *sql.DB) {
11        sql := `
12            CREATE TABLE IF NOT EXISTS photos(
13                    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
14                    src VARCHAR NOT NULL
15            );
16       `
17       
18        _, err := db.Exec(sql)
19        if err != nil {
20            panic(err)
21        }
22    }

In the initialiseDatabase function, we create an instance of the SQLite database using the database file and return that instance. In the migrateDatabase function, we use the instance of the database returned in the previous function to execute the migration SQL.

Let’s create the data structure for our photo and photo collection.

Creating our data structures

The next thing we will do is create the data structure for our object types. We will create a Photo structure and a PhotoCollection structure. The Photo struct will define how a typical photo will be represented while the PhotoCollection will define how a collection of photos will be represented.

Open the main.go file and paste the following code above the initialiseDatabase function:

1type Photo struct {
2        ID  int64  `json:"id"`
3        Src string `json:"src"`
4    }
5    
6    type PhotoCollection struct {
7        Photos []Photo `json:"items"`
8    }

Creating our route handler functions

Next let’s create the functions for our routes. Open the main.go file and paste the following file inside it:

1func getPhotos(db *sql.DB) echo.HandlerFunc {
2        return func(c echo.Context) error {
3            rows, err := db.Query("SELECT * FROM photos")
4            if err != nil {
5                panic(err)
6            }
7            
8            defer rows.Close()
9    
10            result := PhotoCollection{}
11    
12            for rows.Next() {
13                photo := Photo{}
14                
15                err2 := rows.Scan(&photo.ID, &photo.Src)
16                if err2 != nil {
17                    panic(err2)
18                }
19    
20                result.Photos = append(result.Photos, photo)
21            }
22    
23            return c.JSON(http.StatusOK, result)
24        }
25    }
26    
27    func uploadPhoto(db *sql.DB) echo.HandlerFunc {
28        return func(c echo.Context) error {
29            file, err := c.FormFile("file")
30            if err != nil {
31                return err
32            }
33            
34            src, err := file.Open()
35            if err != nil {
36                return err
37            }
38    
39            defer src.Close()
40    
41            filePath := "./public/uploads/" + file.Filename
42            fileSrc := "http://127.0.0.1:9000/uploads/" + file.Filename
43    
44            dst, err := os.Create(filePath)
45            if err != nil {
46                panic(err)
47            }
48    
49            defer dst.Close()
50    
51            if _, err = io.Copy(dst, src); err != nil {
52                panic(err)
53            }
54    
55            stmt, err := db.Prepare("INSERT INTO photos (src) VALUES(?)")
56            if err != nil {
57                panic(err)
58            }
59    
60            defer stmt.Close()
61    
62            result, err := stmt.Exec(fileSrc)
63            if err != nil {
64                panic(err)
65            }
66    
67            insertedId, err := result.LastInsertId()
68            if err != nil {
69                panic(err)
70            }
71    
72            photo := Photo{
73                Src: fileSrc,
74                ID:  insertedId,
75            }
76    
77            return c.JSON(http.StatusOK, photo)
78        }
79    }

In the getPhotos method, we are simply running the query to fetch all the photos from the database and returning them as a JSON response to the client. In the uploadPhoto method we first get the file to be uploaded then upload them to the server and then we run the query to insert a new record in the photos table with the newly uploaded photo. We also return a JSON response from that function.

Adding realtime support to our Go application

The next thing we want to do is trigger an event when a new photo is uploaded to the server. For this, we will be using the Pusher Go HTTP library.

In the main.go file paste the following above the type definitions for the Photo and PhotoCollection:

1var client = pusher.Client{
2        AppId:   "PUSHER_APP_ID",
3        Key:     "PUSHER_APP_KEY",
4        Secret:  "PUSHER_APP_SECRET",
5        Cluster: "PUSHER_APP_CLUSTER",
6        Secure:  true,
7    }

This will create a new Pusher client instance. We can then use this instance to trigger notifications to different channels we want. Remember to replace the PUSHER_APP_* keys with the keys provided when you created your Pusher application earlier.

Next, go to the uploadPhoto function in the main.go file and right before the return statement at the bottom of the function, paste the following code:

    client.Trigger("photo-stream", "new-photo", photo)

This is the code that triggers a new event when a new photo is uploaded to our application.

That will be all for our Go application. At this point, you can build your application and compile it into a binary using the go build command. However, for this tutorial we will just run the binary temporarily:

    $ go run main.go
go-photo-feed-go-run

Building our frontend

The next thing we want to do is build out our frontend. We will be using the Vue.js framework and the Axios library to send requests.

Open the index.html file and in there paste the following code:

1<!doctype html>
2    <html lang="en">
3    <head>
4        <meta charset="utf-8">
5        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css">
7        <title>Photo Feed</title>
8        <style type="text/css">
9            #photoFile { display: none; }
10            #app img { max-width: 100%; }
11            .image-row { margin: 20px 0; }
12            .image-row .thumbnail { padding: 2px; border: 1px solid #d9d9d9; }
13        </style>
14    </head>
15    <body>
16        <div id="app">
17        
18            <nav class="navbar navbar-expand-lg navbar-light bg-light">
19                <a class="navbar-brand" href="#">GoFoto</a>
20                <div>
21                    <ul class="navbar-nav mr-auto">
22                        <li class="nav-item active">
23                            <a class="nav-link" v-on:click="filePicker" href="#">Upload</a>
24                            <input type="file" id="photoFile" ref="myFiles" @change="upload" name="file" />
25                        </li>
26                    </ul>
27                </div>
28            </nav>
29            
30            <div class="container">
31                <div class="row justify-content-md-center" id="loading" v-if="loading">
32                    <div class="col-xs-12">
33                        Loading photos...
34                    </div>
35                </div>
36                <div class="row justify-content-md-center image-row" v-for="photo in photos">
37                    <div class="col col-lg-4 col-md-6 col-xs-12">
38                        <img class="thumbnail" :src="photo.src" alt="" />
39                    </div>
40                </div>
41            </div>
42            
43        </div>
44        <script src="//js.pusher.com/4.0/pusher.min.js"></script>
45        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
46        <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
47    </body>
48    </html>

In the HTML file above we have defined the design for our photostream. We are using Bootstrap 4 and we included the CSS in the HTML above. We are also using the Axios library, Pusher library, and Vue framework. We included the links to the scripts at the bottom of the HTML document.

Next let’s add the Vue.js code. In the HTML file, add the following code right before the closing body tag:

1<script type="text/javascript">
2        new Vue({
3            el: '#app',
4            data: {
5                photos: [],
6                loading: true,
7            },
8            mounted() {
9                const pusher = new Pusher('PUSHER_APP_KEY', {
10                    cluster: 'PUSHER_APP_CLUSTER',
11                    encrypted: true
12                });
13                
14                let channel = pusher.subscribe('photo-stream')
15                
16                channel.bind('new-photo', data => this.photos.unshift(data));
17                
18                axios.get('/photos').then(res => {
19                    this.loading = false
20                    this.photos = res.data.items ? res.data.items : []
21                })
22            },
23            methods: {
24                filePicker: function () {
25                    let elem = document.getElementById('photoFile');
26                    
27                    if (elem && document.createEvent) {
28                        let evt = document.createEvent("MouseEvents");
29                        evt.initEvent("click", true, false);
30                        elem.dispatchEvent(evt);
31                    }
32                },
33                upload: function () {
34                    let data = new FormData();
35                    data.append('file', this.$refs.myFiles.files[0]);
36    
37                    axios.post('/photos', data).then(res => console.log(res))
38                }
39            }
40        });
41    </script>

Above we created a Vue instance and stored the properties photos and loading. The photos property stores the photo list and the loading just holds a boolean that indicates if the photos are loading or not.

In the mounted method we create an instance of our Pusher library. We then listen on the photo-stream channel for the new-photo event. When the event is triggered we append the new photo from the event to the photos list. We also send a GET request to /photos to fetch all the photos from the API. Replace the PUSHER_APP_* keys with the one from your Pusher dashboard.

In the methods property, we added a few methods. The filePicker is triggered when the ‘Upload’ button is pressed on the UI. It triggers a file picker that allows the user to upload photos. The upload method takes the uploaded file and sends a POST request with the file to the API for processing.

That’s all for the frontend, you can save the file and head over to your web browser. Visit http://127.0.0.1:9000 to see your application in action.

Here’s how it will look again:

go-photo-feed-demo

Conclusion

In this article, we have been able to demonstrate how you can use Pusher Channels in your Go application to provide realtime features for your application. As seen from the code samples above, it is very easy to get started with Pusher Channels. Check the documentation to see other ways you can use Pusher Channels to provide realtime features to your users.

The source code for this application is available on GitHub.