Static sites (or Jamstack sites) are great in many ways, from performance to scalability. Instead of creating and maintaining a massive monolith web server, we now can easily split it to directly dealing with microservices APIs from the client-side only. While it is more straightforward working with APIs, there is one challenge - security. Since there is no back-end support for front-end needed, everything, including sensitive information such as authentication credentials, will be exposed to the HTTPS request for a specific API call, either on the request body or in the code itself. For example, using Cloudinary signed upload service for Node.js. The authentication for a signed (secured) upload is a secure protocol created based on user’s API key, API secret key, and cloud name. And you're not supposed to expose these on the client-side.
To solve that, we use a serverless function.
What is a serverless function?
A serverless function is a method that we can trigger using an HTTPS request and does not tie specifically to any web servers. It is hosted and managed by any cloud computing companies (hosting platforms) such as Digital Ocean, Netlify, Vercel, Google Cloud, AWS, etc...
Depending on the hosting platform you are using, you can write a server-function in Node.js, Golang, Java, and so on.
This tutorial will create a secured upload application with a serverless function using Cloudinary, Vercel, and Nuxtjs.
Try it yourself
Setting up
The first step is to make sure Vercel CLI is installed. Vercel CLI allows us to develop and test our serverless functions locally.
1npm i -g vercel #or yarn global add vercel
Next, let's put together a basic Nuxt application scaffold by executing the following command on your terminal:
1#Using npm (v6.1 onwards)2npm init nuxt-app cloudinary-upload-app3456#Or using YARN7yarn create nuxt-app cloudinary-upload-app891011#Or using npx12npx create-nuxt-app cloudinary-upload-app
And select the configuration options accordingly. You can choose anything to suit your needs, but I recommend the following for our starter application:
- UI framework: Tailwind CSS - we use TailwindCSS to provide a basic beautiful CSS setup without extra configuration.
- Axios - we use this module to send API requests to the serverless function.
- Linting tools: Prettier, Lint staged files.
- Rendering mode: Universal (SSR/SSG) - we want Nuxt to act as a Static site generator for our application during development. This mode is required to turn on the static deployment static.
- Deployment target: Static - this is important as we want to deploy our application as static site.
Once it's ready, we can navigate to the project directory and have it connected to Vercel using:
1cd cloudinary-upload-app && vercel
and set up your project linking accordingly. See Vercel's project settings for more information.
⚠️ You may need to login using vercel login
to connect to your Vercel account.
Then run the following to have the app up and running:
1vercel dev
You can see how the app looks on the browser by holding CMD (Mac) or Ctrl and clicking on the port link displayed in the terminal:
Now we can start to create our upload component.
Upload component
In the app directory, create a new file Upload.vue
under components
directory with the following skeleton code:
This is our Upload component.
Next we will create the component skeleton by adding the following code:
1<template>2 <div class="p-4 w-full">3 <h1 class="text-xl font-semibold text-blue-700 text-center">4 Upload with Cloudinary Demo5 </h1>6 <div class="my-4 mx-auto text-center">7<!--This is our upload component-->8<upload />9</div>10</div>11</template>
Receive user input
To allow user to select a local file for uploading, we use input
element with the following attributes:
type="file"
- indicate this input field is meant to let users choose a file from their device storage.accept=".jpeg,.jpg,.png,image/png,image/png,.svg,.gif"
- indicate the target file type the input element should accept. By setting it to accept only image types, we make sure users will only select the right file format for our component to handle.- A bit of CSS styling by assigning Tailwind class names to
class
In template
section of Upload.vue
, our code looks like:
1<input2type="file"3accept=".jpeg,.jpg,.png,image/jpeg,image/png,.svg,.gif"4class="p-3 border cursor-pointer"5aria-label="upload image button"6/>
The output in browser becomes:
Now we need to declare a method selectFile
in methods
fields of the component's JavaScript logic located in <script>
section:
1export default {2methods: {3selectFile(e){}4}5}
Then bind that method as a listener to change
event in the input field
1<input2type="file"3accept=".jpeg,.jpg,.png,image/jpeg,image/png,.svg,.gif"4class="p-3 border cursor-pointer"5@change="selectFile"6/>
Once a user clicks on Choose File
and selects a file, selectFile
will be triggered with e
representing the event change
's data as a parameter.
Handling the user input
We can get the list of selected files by accessing e.target.files
, which contains File
objects. Since we allow users to upload a single file, we only need to care about the first file on the list - files[0]
1const file = e.target.files[0]
To upload using HTTPs, we first need to read and convert the received file's data to base64 format. For that, we use the built-in FileReader
of the File API for the web and perform the following:
- Load the file data using
FileReader
- Translate its data as base64 using
FileReader.readAsDataURL
method
Since these processes are asynchronous, we will use Promise API to wrap around them:
1const readData = (f) =>2new Promise((resolve) => {3const reader = new FileReader()4reader.onloadend = () => resolve(reader.result)5reader.readAsDataURL(f)6})
And use await/async
to make sure we get the output data before moving on to the next step.
1const data = await readData(file)
To this point, we get our data ready to be sent and upload. And our implementation code for selectFile()
becomes:
1async selectFile(e) {2const file = e.target.files[0]34/* Make sure file exists */5if (!file) return;67const readData = (f) =>8new Promise((resolve) => {9const reader = new FileReader()10reader.onloadend = () => resolve(reader.result)11reader.readAsDataURL(f)12})13const data = await readData(file)14}
So far, so good? The next step is to build a serverless function for uploading.
Add a serverless upload function
Vercel provides excellent support for serverless functions written in different backend languages. In our demo app, we use Node.js and Cloudinary Node.js SDK to write our upload serverless function.
We add the api
folder to the root of the project. By default, Vercel will detect the serverless functions based on this directory and deploy them accordingly.
Let's add an upload.js
file to the api
folder, with the following content:
1module.exports = async (req, res) => {2/* logic implementation here */3}
Great. Now we need to install Cloudinary Node.js SDK as project dependency and start using it:
1npm i cloudinary #OR yarn add cloudinary
Setting up Cloudinary account
To have the Cloudinary upload service working, we need to config it with cloudName
, api_key
and api_Secret
:
1const sdk = require('cloudinary').v22345sdk.config({6cloud_name: 'your-cloud-name',7api_key: 'your-api-key',8api_secret: 'your-api-secret',9})
You can find these keys at the your Cloudinary Dashboard page or Settings page:
Then we can execute an upload under the registered Cloudinary account based on the req.body.file
received from the HTTPs request:
1module.exports = async (req, res) => {2const body = req.body3const uploader = sdk.uploader4567const resource = await uploader.upload(body.file)89return res.json(resource)10}
Our complete code for upload.js
will be:
1const sdk = require('cloudinary').v22345sdk.config({6cloud_name: 'your-cloud-name',7api_key: 'your-api-key',8api_secret: 'your-api-secret',9})10111213module.exports = async (req, res) => {14const body = req.body15const uploader = sdk.uploader16171819const resource = await uploader.upload(body.file)2021return res.json(resource)22}
That's it. Let's go back to our Upload
component and connect its selectFile
to this serverless function, shall we?
Calling the upload server-less function
Inside selectFile()
implementation of Upload.vue
, we call the serverless function through a HTTPS POST request using $axios
1const resource = await this.$axios.$post('/api/upload', {2file: data3})
And if the upload is completed successfully, we will receive a JSON object containing all relevant data for the uploaded image. Otherwise, it will return a JSON object containing error
with msg
field indicating the error.
Making the UI informative
To make our UI be more informative during the uploading process, let's add a loading
, image
and error
variables to data
section of the component.
1data() {2return {3loading: false,4error: null,5image: null, //contain the uploaded image6}7},
And assign values them accordingly during selectFile()
execution:
1async selectFile(e) {2const file = e.target.files[0]34/* Make sure file exists */5if (!file) return;6789this.loading = true //Start loading1011const readData = (f) =>12new Promise((resolve) => {13const reader = new FileReader()14reader.onloadend = () => resolve(reader.result)15reader.readAsDataURL(f)16})17const data = await readData(file)18const resource = await this.$axios.$post('/api/upload', {19file: data20})2122this.loading = false //End loading2324if (resource.error) {25this.error = resource.error26return27}2829this.image = resource30}
Then add the HTML code to handle the following:
- Disable the input field when an upload is going on.
- Display an "Uploading..." message when an upload is going on
- Display an error message when something is wrong.
- Display the uploaded image otherwise.
1<div class="upload-comp">2<input3type="file"4accept=".jpeg,.jpg,.png,image/jpeg,image/png,.svg,.gif"5class="p-3 border cursor-pointer"6@change="selectFile"7:disabled="loading"8/>9<div class="m-5 text-lg font-thin italic" v-show="loading">10Uploading...11</div>12<div v-if="error && !loading" class="m-5">{{ error.msg }}</div>13<img14v-if="!loading && image"15:src="image.secure_url"16class="m-5"17>18</div>
Now our complete code for Upload.vue
is:
template
section:
1<template>2<div class="upload-comp">3<input4type="file"5accept=".jpeg,.jpg,.png,image/jpeg,image/png,.svg,.gif"6class="p-3 border cursor-pointer"7@change="selectFile"8:disabled="loading"9/>10<div class="m-5 text-lg font-thin italic" v-show="loading">11Uploading...12</div>13<div v-if="error && !loading" class="m-5">{{ error.msg }}</div>14<img15v-if="!loading && image"16:src="image.secure_url"17class="m-5"18>19</div>20</template>
script
section
1<script>2export default {3data() {4return {5loading: false,6error: null,7image: null,8}9},10methods: {11async selectFile(e) {12const file = e.target.files[0]1314/* Make sure file exists */15if (!file) return;16171819this.loading = true2021const readData = (f) =>22new Promise((resolve) => {23const reader = new FileReader()24reader.onloadend = () => resolve(reader.result)25reader.readAsDataURL(f)26})27const data = await readData(file)28const resource = await this.$axios.$post('/api/upload', {29file: data30})3132this.loading = false33343536if (resource.error) {37this.error = resource.error38return39}4041this.image = resource42}43}44}45</script>
And finally, you can simply run the following to have your application deploy to Vercel on production.
1vercel --prod
Summary
At this point you have a functional application from front-end to back-end to securely upload an asset with the least set up required. The next step is to play around with the upload configurations from Cloudinary to pre-generate transformations, or getting smart color detection, and so on. And then using the Cloudinary module for Nuxt to dynamically display the uploaded asset with optimization on the client-side.