DEV Community

Cover image for How to Upload Base64 Images in Vue & NodeJS
Martins Onuoha
Martins Onuoha

Posted on

How to Upload Base64 Images in Vue & NodeJS

Previously, I had written on uploading multiple images from a Vue Application to a Laravel Backend. In that solution, we made use of Javascript’s FormData Object. Here’s that article if you missed it:

In this solution, however, we would use Javascript’s FileReader API, which allows us to read the content of image files and handle those files in base64 format. Regardless of the backend language being used, base64 encoded images can be decoded on the server and stored locally in the filesystem (or on cloud storage).


Image path

Here’s some illustration to better understand the process of uploading a Base64 image. While interfacing with your web application, the user can select an image (using the input tag, with a type of file) from their device; the content of the selected image is read using the FileReader API, and we send the base64 string in Data URL format to a backend API.

On the backend, the base64 string is decoded from the string format to its original file object, and we can then store the decoded file and return the storage path/URL to the frontend.

Down to the Base-ics

What on earth is Base64?

Base64 or Radix 64 is a binary-to-text encoding system that is designed to allow binary data to be represented in ASCII string format. One common application of base64 encoding on the web is to encode binary data so it can be included in a data: URL.

Data: URL?

Data URLs are URLs that are prefixed with “data:”. They allow embedding of file content inline in documents (.html). Data URLs are composed of 3 major parts after the “data:” prefix:

data:[<mediatype>][;base64],<data>
Enter fullscreen mode Exit fullscreen mode

The [<mediatype>] part refers to the file format, and it could contain any one of the following values:

text/plain — for text files

image/jpeg or image/jpeg — for .png and .jpeg image files

application/pdf — for .pdf files etc.

The [<base64>] part is optional and can be ignored if the data is non-textual. The last part is the textual (or non-textual) file content. Technically, this is the format in which we would send out images.


FileReader API

What’s this?

The FileReader object lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user’s computer, using File or Blob objects to specify the file or data to read. — MDN

When a user selects a file using the input tag, a “change” event is triggered; this event object contains a FileList object that lets you access the list of files selected with the <input type="file"> element.

The FileReader API has 4 methods for reading file contents:

The method we’re concerned with is the readAsDataURL method, as it allows us to read the content of a Blob file and return an object with a result property containing a data: URL representing the file’s data.


Getting Started

First, be sure you have all the Vuejs requirements installed. We will be using the Vue CLI to serve our application; however, we won’t be scaffolding an entire Vue application with the CLI. Thanks to Vue CLI’s instant prototyping, we can serve a single Vue file.

Let’s set up our project directory.

Project directory

instant prototyping](https://cli.vuejs.org/guide/prototyping.html), we can serve a single Vue file.

Let’s set up our project directory.

1_ly5trp9-hqpbxqxdobjzng.gif

mkdir vue-base64 vue-base64/assets vue-base64/assets/styles
Enter fullscreen mode Exit fullscreen mode
touch vue-base64/Index.vue && touch vue-base64/assets/styles/main.css
Enter fullscreen mode Exit fullscreen mode

Our project directory would look something like this:

.
|--vue-base64
  |-- Index.vue
  |-- assets
    |-- styles
        |--main.css
Enter fullscreen mode Exit fullscreen mode

We’ll write a basic structure for our image and input elements in our template section.

Template

<template>
  <div>
    <div class="container mt-10">
      <div class="card bg-white">
        <img :src="image" alt="card_image">
        <input @change="handleImage" class="custom-input" type="file" accept="image/*">
      </div>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Here, we create a card to house our image and input button. On the input tag, we’re listening for a change event and calling a “handleImage” method every time that event is fired. We’re also binding the image “src” attribute to a reactive property “image” (We’ll add this property to our data object under the script section), so the image is updated every time a new one is selected.

We’ll add a bit of styling in our CSS file (main.css).

CSS

* {
  font-family: Arial, Helvetica, sans-serif;
}
body {
  background: #d8dddb;
}
.container  {
  display: flex;
  justify-content: center;
}
.mt-10 {
  margin-top: 10rem;
}
.bg-white {
  background: #fff;
}
.card {
  height: 10rem;
  width: 20rem;
  border-radius: 10px;
  padding: 20px;
  text-align: center;
}
img {
  width: 17rem;
}
Enter fullscreen mode Exit fullscreen mode

Import the CSS file main.css into Index.vue like so;

<style>
  @import './assets/style/main.css';
</style>
Enter fullscreen mode Exit fullscreen mode

Script

<script>
import axios from 'axios';

export default {
  name: 'home',
  data() {
    return {
      image: '',
      remoteUrl: ''
    }
  },
  methods: {
    handleImage(e) {
      const selectedImage = e.target.files[0]; // get first file
      this.createBase64Image(selectedImage);
    },
    createBase64Image(fileObject) {
      const reader = new FileReader();

      reader.onload = (e) => {
        this.image = e.target.result;
        this.uploadImage();
      };
      reader.readAsDataURL(fileObject);
    },
    uploadImage() {
      const { image } = this;
      axios.post('http://127.0.0.1:8081/upload', { image })
        .then((response) => {
          this.remoteUrl = response.data.url;
        })
        .catch((err) => {
          return new Error(err.message);
        })
    }
  },
}
</script>
Enter fullscreen mode Exit fullscreen mode

In the script section, the first thing we need to do is declare the reactive property (image) in our data object. We’ll then declare the handleImage method to respond to the on-change event triggered by the input tag; we grab the first Blob from the FileList object and assign it to a variable.

Vue file

If you inspect the result generated using the Vue DevTool, you’ll notice the file is now in the Data URL format.

File

Note: The <img> tag element’s src attribute can contain the URL-address or the data URL of an image.


Backend (Node.js)

This implementation isn’t limited to just Node.js or Javascript. Using the right library or built-in APIs, you can convert the base64 string to an image file and store it in your application’s filesystem with basically any backend language of choice.

Since Vue.js uses the Nodejs runtime in the background, we can have our server-side in the same project directory so both applications can share libraries. Let’s make a bit of adjustment to the project folder. From the root directory, we’ll do a basic setup for a Node.js Express server.

npm init -y && mkdir server server/public && touch server/app.js
Enter fullscreen mode Exit fullscreen mode

Our project directory should now look something like this:

    |-- Index.vue
    |-- assets
        |-- styles/main.css
    |-- server
        |-- public
        |-- app.js
    `-- package.json
Enter fullscreen mode Exit fullscreen mode

We’ll install all the necessary packages. Here’s what we need:

  • Express Nodejs web framework

  • Body-parser: to parse incoming request bodies.

  • Axios: to send HTTP requests from our Vue application

  • Cors: so we can send requests between the same origin

  • Base64-img: to Convert images to base64, or convert base64 to images

npm i base64-img axios cors express body-parser
Enter fullscreen mode Exit fullscreen mode

Once installed, open up app.js, set up the express server, and create the route to handle image upload, decoding, and storage.

const express = require('express');
const app = express();
const base64Img = require('base64-img');
const bodyParser = require('body-parser');
const cors = require('cors')
const port = 8081;

app.use(cors())
app.use(express.static('./server/public'))
app.use(bodyParser.json({ limit: '50mb' }));

app.post('/upload', (req, res) => {
  const { image } = req.body;
  base64Img.img(image, './server/public', Date.now(), function(err, filepath) {
    const pathArr = filepath.split('/')
    const fileName = pathArr[pathArr.length - 1];

    res.status(200).json({
      success: true,
      url: `http://127.0.0.1:${port}/${fileName}`
    })
  });
});

app.listen(port, () => {
  console.info(`listening on port ${port}`);
})
Enter fullscreen mode Exit fullscreen mode

Here, we’re telling our express server to use cors, JSON body-parser (it’s important to set the limit, as base64 images can be really large). We’re also setting the ./server/public folder as our static folder (we’ll store our decoded images here).

Next, we’re setting up the server to respond to a POST request on the “/upload” endpoint by getting the image string from the request body, passing the image string into the base64Img.img(…) function (this function takes 4 arguments; base64 string, the path to store the image, filename, and a callback function that returns the file path. Finally, we grab the file name from the path and return the URL to the user.

Note: we’re using the Date.now() function as the file name just to maintain a level of uniqueness. Since the function returns the number of milliseconds elapsed since January 1, 1970, it’s almost impossible to get the same value everytime the function is called.


Let’s make some changes to our Vue application.

...
data() {
  return {
    image: '',
    remoteUrl: ''
  }
},
...
Enter fullscreen mode Exit fullscreen mode

Add a new property, remoteUrl, to the data object, this will hold the value of the URL returned by our server. Next, we need to import Axios and create a method to handle our post request to the server.

uploadImage() {
  const { image } = this;
  axios.post('http://127.0.0.1:8081/upload', { image })
    .then((response) => {
      this.remoteUrl = response.data.url;
    })
    .catch((err) => {
      return new Error(err.message);
    })
}

Well call this method within our `reader.onload(…)` event.

...
reader.onload = (e) => {
    this.image = e.target.result;
    this.uploadImage();
};
...
Enter fullscreen mode Exit fullscreen mode

Finally, Let’s render the image URL returned by the server just to be sure we got the right file from the server. Add this below the .container div in your template section:

  <div class="mt-10" style="text-align: center">
      <h3>SERVER IMAGE</h3>
      <img :src="remoteUrl" alt="">
  </div>
Enter fullscreen mode Exit fullscreen mode

Your Index.vue file should look something like this:

<template>
  <div>
    <div class="container mt-10">
      <div class="card bg-white">
        <img style="" :src="image" alt="">
        <input @change="handleImage" class="custom-input" type="file" accept="image/*">
      </div>
    </div>
    <div class="mt-10" style="text-align: center">
      <h3>SERVER IMAGE</h3>
      <img :src="remoteUrl" alt="">
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  name: 'home',
  data() {
    return {
      image: '',
      remoteUrl: ''
    }
  },
  methods: {
    handleImage(e) {
      const selectedImage = e.target.files[0]; // get first file
      this.createBase64Image(selectedImage);
    },
    createBase64Image(fileObject) {
      const reader = new FileReader();

      reader.onload = (e) => {
        this.image = e.target.result;
        this.uploadImage();
      };
      reader.readAsDataURL(fileObject);
    },
    uploadImage() {
      const { image } = this;
      axios.post('http://127.0.0.1:8081/upload', { image })
        .then((response) => {
          this.remoteUrl = response.data.url;
        })
        .catch((err) => {
          return new Error(err.message);
        })
    }
  },
}
</script>
<style>
  @import './assets/style/main.css';
</style>
Enter fullscreen mode Exit fullscreen mode

Piecing it all together

In our package.json, we’ll add two scripts, one to start our Vue application and the other to start our express server.

{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "ui": "vue serve Index.vue",
    "server": "node ./server/app.js"
  },
}
Enter fullscreen mode Exit fullscreen mode

From here, you can open up two tabs on your terminal and run both scripts.

npm run ui

npm run server
Enter fullscreen mode Exit fullscreen mode

You should have your server running on port 8081 and your Vue application on port 8080. Navigate to your Vue app and test.

Test

As mentioned previously, you can implement base64 image decode in any preferred language. Feel free to experiment with the source code here:

GitHub logo MartinsOnuoha / vue-base64

Handling base64 Images in Vue

Cheers ☕️


Top comments (0)