Build Instagram Using TypeScript, Node, Express and Vue - Part 5
Tags: node, vue, javascript, typescript
Published: 2020-06-10 18:54:58

This is tutorial 5 of a 5-part tutorial, but each tutorial can be read in isolation to learn various aspects Node+Express+TypeScript+Vue API/Vue web app set up. 

## Advanced Vue templating and image uploading to Express

<sub>Looking to learn mobile/desktop apps? The skills and concepts here are fundamental and re-usable for mobile apps (NativeScript) or desktop apps (Electron). I may cover them as a follow-on.</sub>

#### Navigation to other parts (you are at *part 5*)
1. [Setting up Node and Express API with TypeScript](https://dev.to/calvintwr/build-instagram-using-typescript-node-express-and-vue-part-1-57do)
2. [Setting up VueJs with TypeScript](https://dev.to/calvintwr/build-instagram-using-typescript-node-express-and-vue-part-2-k48)
3. [Setting up Postgres with Sequelize ORM](https://dev.to/calvintwr/build-instagram-using-typescript-node-express-and-vue-part-3-4d1n)
4. [Basic Vue templating and interaction with API](https://dev.to/calvintwr/build-instagram-using-typescript-node-express-and-vue-part-4-23ll)
5. Advanced Vue templating and image uploading to Express

If you have not, you can start building by cloning and checking out tutorial-part4 branch:
```
git clone https://github.com/calvintwr/basicgram.git
git checkout tutorial-part4
```

In this tutorial you will complete the last feature, which is to upload a post with image with Basicgram app, build the Express API endpoint to receive the image, and another endpoint to produce a post feed, and finally the Vue templating to display them to complete the whole loop.

## 1. Image resizing

First, you want to resize images on client-side before uploading. This means using a Javascript browser resizer, which can sound like a bad idea at first, but it really isn't, given this context. On balance, client-side resized images allows faster upload time, reduced server bandwidth consumption, and flexibility to allow users to practically dump images in right from a DSLR. And in fact it is not that slow, with pretty good image results [`Blitz`](https://github.com/calvintwr/blitz-hermite-resize):
```
npm install blitz-resize --save
```

```js
const Blitz = require('blitz-resize')
let blitz = Blitz.create()

blitz.resize({
    source: file or event,
    height: 640,
    width: 640,
    output: 'jpg', // or png or gif etc,
    outputFormat: image/canvas/data/blob/download,
    quality: 0.8 // 80%
}).then(output => {}).catch(err => {})
```

### On [Blitz](https://github.com/calvintwr/blitz-hermite-resize) and image manipulation/uploading
For data types in image manipulation, there are commonly 2 types. First is `dataURI` which comes in handy and can be attached to `<img>`'s src like this:

```html
<!-- single quote due to XSS Markdown restrictions -->
<img src=`data:image/png;base64,iVBORw0KGgo...`>
```

Second is the `Blob` format, which is used for uploading over HTTP/HTTPS.

[Blitz](https://github.com/calvintwr/blitz-hermite-resize) can provide for either by using `output: 'data'` or `output: 'blob'`, but we will see later how this comes in handy.

## 2. Code `camera.vue` to prepare to post stuff:

```vue
<!-- camera.vue -->
<template>
    <v-ons-page>
        <div class="container text-center  mx-auto p-1">
            <!-- attach the #readFile method to change event -->
            <input 
                type="file" 
                capture="camera" 
                accept="image/*" 
                id="cameraInput" 
                name="cameraInput"
                @change="readFile" 
            >
            <img class="py-2" ref="image">
            <!-- `ref` defined for this textarea is a Vue reference which will be handy -->
            <textarea 
                class="py-2 w-full textarea" 
                rows="3" 
                placeholder="Write your caption"
                ref="caption"
            ></textarea>

            <!-- #post is for uploading the post -->
            <button 
                class="my-2 button"
                @click="post" 
                :disabled="buttonDisabled"
            >Post</button>
        </div>
        
    </v-ons-page>
</template>

<script lang="ts">
import Vue from "vue"
import Blitz = require('blitz-resize')
import * as superagent from 'superagent'
const blitz = Blitz.create()

export default {
    props: {
        userName: {
            type: String
        },
        userID: {
            type: Number
        }
    },
    data() {
        return {
            image: { type: Blob }, // this is to store our image
            buttonDisabled: true // a flag to turn our button on/off
        }
    },
    methods: {
        readFile(event) {
            let file = event.srcElement.files[0] // this is where HTML file input puts the file
            let self = this
            let output;

            // super fast resizing 
            blitz({
                source: file,
                height: 640,
                width: 640,
                outputFormat: 'jpg',
                // we will use data because we want to update the image in the DOM
                output: 'data', 
                quality: 0.8
            }).then(data => {

                // update the image so that user sees it.
                self.$refs["image"].src = data

                // prepare the Blob. Blitz internally has a #dataURItoBlob method.
                self.image = Blitz._dataURItoBlob(data) 
    
                self.buttonDisabled = false
            }).catch(err => {
                console.log(err)
            })

        },
        post(event) {
            let self = this
            this.buttonDisabled = true
            let caption = this.$refs["caption"].value // using Vue's $ref property to get textarea.

            // Note: To upload image, the request type will be "multipart"
            // Superagent automatically takes care of that and you need to
            // use `field` for text/plain info, and `attach` for files 
            superagent
                .post('http://localhost:3000/posts/add')
                .field('userID', this.userID)
                .field('caption', caption)
                .attach('photo', this.image)
                .then((res: superagent.Response) => {
                    alert('Successful post. Go to your profile to see it.')
                }).catch((err: Error) => {
                    this.buttonDisabled = false
                    alert(err)
                })
        }
    }
}
</script>
```

## 3. Prepare the API to receive the post

So now that our view is ready to post, we need to create the API endpoint of `localhost:3000/posts/add`.

Before we write the code, we should think about where to upload the files. The natural choice is to put under "public/uploads", but remember that in [tutorial 1](https://dev.to/calvintwr/build-instagram-using-typescript-node-express-and-vue-part-1-57do), we set up the TypeScript compiler to delete the whole "dist" folder (which also has public folder in it) before compiling into it. This will delete all our uploaded images with every compile.

So you have to move the public folder out to see on the same level with "api" and src, like this:

![Express folder structure - move public folder out of "src"](https://dev-to-uploads.s3.amazonaws.com/i/xj7ymp7tvafvcups9gza.png)

Also, you need to tell Express that you have changed the public folder, which is where by default it serves its static files from:
```js
/* api/src/app.ts */

// change
app.use(express.static(join(__dirname, 'public')))

// to
app.use(express.static(join(__dirname, '../public')))
```

Express doesn't handle "multipart" requests, so we will need a module. The best one lying around is `formidable`. You also have `multer` and `busboy`, but again, I find `formidable`'s syntax to be the most friendly.

#### Install formidable: 

```
npm install formidable --save
npm install @types/formidable --save-dev
```

Formidable's syntax is pretty flexible, and is event-driven. So the idea is to attach functions to events. For example, when a HTTP receive completes all data transmission, formidable will emit the event `end`, which you will use like this:

```js
const form = formidable()
function formEndHandler():void { perform some action. }
form.on('end', formEndHandler)
```

So with this in mind, we will create `routes/posts.ts`:

#### `posts.ts`:
```ts
import express from 'express'
import { Fields, Files, File } from 'formidable' // typing
import { join } from 'path' // we will use this for joining paths
const formidable = require('formidable') // formidable

const router = express.Router()
const Not = require('you-are-not')
const not = Not.create()
const DB = require('../models/index')

router.get('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
    // get all posts
})

router.post('/add', (req: express.Request, res: express.Response, next: express.NextFunction) => {

    const form = formidable({ multiples: true })

    let params: any
    form.parse(req, (err: Error, fields: Fields, files: Files) => {
        params = fields

        // use Not to sanitise our received payload

        // define a schema
        let schema = {
            userID: ['string', 'number'],
            caption: ['string']
        }

        // sanitise it
        let sanitised = Not.checkObject(
            'params',
            schema, 
            params, 
            { returnPayload: true }
        )
        
        // if sanitised is an array, we will throw it 
        if(Array.isArray(sanitised)) {
            throw Error(sanitised.join(' | ')) // join the errors
        }
        params = sanitised
    })

    let fileName: string;
    form.on('fileBegin', (name: string, file: File) => {
        fileName = name + (new Date().getTime()).toString() + '.jpg'
        file.path = join(__dirname, '../../public/uploads', fileName)
    })

    form.on('error', (err: Error) => {
        next(err) // bubbble the error to express middlewares
    })

    // we let the file upload process complete before we create the db entry.
    // you can also do it asynchronously, but will require rollback mechanisms
    // like transactions, which is more complicated.
    form.on('end', () => {
        return DB.Post.create({
            User_userID: params.userID,
            image: fileName,
            caption: params.caption
        }).then((post: any) => {
            console.log(post)
            res.status(201).send(post)
        }).catch((err: Error) => {
            next(err)
        })
    })
})

module.exports = router
```

Restart your server and head over to your view, and you should be able to do this:

![Simulating Instagram: Client-side resizing, express+nodejs file handling](https://dev-to-uploads.s3.amazonaws.com/i/mvgj0vg06coi9djw9ves.gif)

If you realise, the resizing was very fast, and likewise the upload time, as the file size is drastically reduced with client-side compression by [Blitz](https://github.com/calvintwr/blitz-hermite-resize).

Now we just need to create the endpoints for a user to get all his/her posts, for the profile page, and also make the post feeds for the home page.

## 4. Profile page `profile.vue` and API endpoint

#### You should be pretty good by now. The endpoint (which we will name `GET /posts/own`) to get all posts of a user is not hard at all:
```ts
/* routes/posts.ts */

router.get('/own', (req: express.Request, res: express.Response, next: express.NextFunction) => {

    // we will receive userID as a string. We want to parse it and make sure
    // it's an integer like "1", "2" etc, and not "1.1", "false"
    Not.defineType({
        primitive: 'string',
        type: 'parseable-string',
        pass(id: string) {
            // TypeScript does not check at runtime. `string` can still be anything, like null or boolean.
            // so you need Notjs.
            return parseInt(id).toString() === id
        }
    })

    // for GET, the standard is to use querystring.
    // so it will be `req.query` instead of `req.body`
    not('parseable-string', req.query.userID)  

    DB.Post.findAll({
        where: {
            User_userID: req.query.userID
        },
        order: [ ['id', 'DESC'] ] // order post by id in descending, so the latest will be first.
    }).then((posts:any) => {
        res.send(posts)
    }).catch((err:Error) => {
        next(err)
    })
})

```

### The low-down on VueJS's Hooks: #created(), #mounted() etc...

Next is `profile.vue`. 

VueJS provides for [several "hooks"](https://lavalite.org/blog/created-and-mountedin-vuejs) which are called to prepare the view. They look like this:
```vue
<template>
    <div> {{ dataFromAPI }} </div>
</template>
<script>
import Vue from 'vue'
export default {
    data() {
        return {
            // this is bound to {{ dataFromAPI }} in the DOM
            dataFromAPI: 'Waiting for API call'
        }
    },
    // or created(), depending on when you want it.
    mounted() {
        // anything inside here gets called when this view is mounted

        // you will fetch some data from API.

        // suppose API results the results, then doing this:
        this.dataFromAPI = results
        // will update the value in {{ dataFromAPI }}
    }
}
</script>
```

#### The most commonly used are `created()` and `mounted()`. We are going to code `profile.vue` like this:
```vue
<!-- profile.vue -->
<template>
    <v-ons-page>
        <div class="content">
            <div class="w-full p-10" style="text-align: center">
                {{ userName }}'s Profile
            </div>

            <!-- Three columns Tailwind class-->
            <div v-if="posts.length > 0" class="flex flex-wrap -mb-4">
                <div 
                    class="w-1/3"
                    v-for="post in posts" 
                    :key="post.id"
                ><img :src="'http://localhost:3000/uploads/' + post.image"></div>
            </div>    
        </div>
    </v-ons-page>
</template>

<script lang="ts">
import Vue from 'vue'
import * as superagent from 'superagent'

export default {
    props: {
        userName: {
            type: String
        },
        userID: {
            type: Number
        }
    },
    data() {
        return {
            posts: { type: Array }
        }
    },
    mounted() {
        superagent
            .get('http://localhost:3000/posts/own')
            .query({ userID: this.userID })
            .then((res: superagent.Response) => {
                // attach the results to the posts in our data
                // and that's it! Vue will update the DOM because it's binded
                this.posts = res.body
            }).catch((err: Error) => {
                alert(err)
            })
    }
}
</script>
```
Explanation: That just tells Vue that when this view is **mounted**, please run the **superagent** request for me.

>Tip: For some very odd reasons, OnsenUI needs all your content to be wrapped in `<div class="content">`, if not things will start to behave funny.

>Tip: Notice that we wrap the posts with `<div v-if="posts.length > 0">`. This is to prevent Vue from rendering the DOMs which requires data but the API call has not yet completed. If you don't do that, nothing will break, just that you will see some pesky console log errors telling you that an image url is broken, for example.

For simplicity, we will skip the triggers that will update the views when say, you upload a post. Right now you will have to refresh the whole app.

And you should see:

![VueJS template data-binding and updating via AJAX/API calls](https://dev-to-uploads.s3.amazonaws.com/i/b9z09vbe1rpglqfek1zf.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/r0583luir5l3pvhps7mz.png)

#### We will do something similar for the `homepage.vue`, using #created(), which will be called a little earlier:

```vue
<template>
    <v-ons-page>
        <div class="content">
            <div v-if="posts.length > 0">
                <v-ons-card v-for="post in posts" :key="post.id">
                    <img class="w-full" :src="'http://localhost:3000/uploads/' + post.image">
                    <div class="py-1 content">
                        <p class="text-xs font-bold py-2">{{ post.User.name }}<p>
                        <p class="text-xs text-gray-700">{{ post.caption }}</p>

                    </div>
                </v-ons-card>
            </div>
        </div>
    </v-ons-page>
</template>

<script lang="ts">
import Vue from 'vue'
import * as superagent from 'superagent'

export default {
    props: {
        userID: {
            type: Number
        }
    },
    data() {
        return {
            posts: { type: Array }
        }
    },
    created() {
        superagent
            .get('http://localhost:3000/posts/feed')
            .query({ userID: this.userID })
            .then((res: superagent.Response) => {
                this.posts = res.body
            }).catch((err: Error) => {
                alert(err)
            })
    }
}
</script>
```

#### And our "/posts/feed" inside of `routes/post.ts` API:
```ts
router.get('/feed', (req: express.Request, res: express.Response, next: express.NextFunction) => {

    not('parseable-string', req.query.userID)  

    // user's feed is not his/her own posts
    DB.Post.findAll({
        where: {
            User_userID: {
                // this is a Sequelize operator
                // ne means not equal
                // so this means from all post that
                // doesn't belong to this user.
                [DB.Sequelize.Op.ne]: req.query.userID
            }
        },
        // we want to include the User model for the name
        include: [ DB.User],
        order: [ ['id', 'DESC'] ] // order post by id in descending, so the latest will be first.
    }).then((posts:any) => {
        res.send(posts)
    }).catch((err:Error) => {
        next(err)
    })
})
```
But after doing that, you realise that your app isn't sending `userID` the API. That is because we did not pass the `userID` prop to `homepage.vue`. We can fix that by editing `home.vue`:
```js
icon: 'fa-home',
label: 'Home',
page: homePage,
key: "homePage",
props: {
    userID: {
        type: Number // add the userID prop to homePage
    }
}
```
> Tip: You will realise that your app quickly outgrows Vue's basic mechanism of passing data around via `props` and event emitters. This is why you almost always need [`Vuex`](https://vuex.vuejs.org/) for state management, to store data accessible by the whole app in one place.

And it should work:

![Build Instagram with Express and Vue in TypeScript](https://dev-to-uploads.s3.amazonaws.com/i/3v49opgbeszv6xw7y24f.png)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qhzq2esa8eigombos5he.gif)

And there you have it! A super sketchy Instagram.

You can head over to the git repo to clone this completed app to play with it:
```
git clone https://github.com/calvintwr/basicgram.git
```