How to Build a Vue.js Portfolio with Axios and Flickr APIStep by step guide to creating your first Vue.js portfolio app. We install and configure Vue.js, create a new project and build a portfolio website using Flickr API.

We'll create a simple vue.js portfolio-style website in this tutorial series using Axios and the Flickr API. This series aims to practice basic Vue concepts and gain exposure to working with an API. I am assuming a basic working knowledge of HTML, CSS, and Javascript.

The source files for this tutorial are available on GitHub at the end of the article.
Installing Vue.js CLI
Vue CLI requires Node.js version 8.9 or above. If you haven't got Node installed, head over to the Node download page and follow the installation instructions to install Node.
To install the Vue.js package, enter the following command into PowerShell (or terminal) with administrator privileges.
npm install -g @vue/cli
After installation, you can access the vue
binary in your command line. You can verify that it is correctly installed by simply running vue
, which should present you with a help message listing all available commands.
Vue.js Portfolio Project Setup
Now, we get to create our Vue.js portfolio project. Navigate to where you want to save (e.g. c:\dev\
) it in , then using your PowerShell entering the following:
vue create portfolio
The PowerShell window will show a wizard-style installer. Select the following options.


Once that is done, it will create a folder called portfolio
in c:\dev\
(or wherever you ran the command) and create a sample template.
CD into the new portfolio
folder and run this command to confirm the project was successfully created.
npm run dev
If everything has worked successfully, you will see the following in your browser:

Now, in the PowerShell window, hit Ctrl + C to stop the server from running. We need to install a few packages to get our app running.
First, we will use axios to handle our AJAX requests to the Flickr API. Install the package by running this command in the PowerShell window.
npm install axios --save
Next, we are going to use the moment package to format dates for us. Install moment
using this command.
npm install moment --save
If all goes well, you should see a successful message.
Flickr API Setup
Working with the Flickr API requires a key. This key is a unique identifier that lets Flickr know we're a legitimate source making a request. The key is free, but you do have to have a Flickr account.
Head over to Flickr and log in (register for an account if you don't have one), then visit the Flick API Services page. Take a moment to read the guides under "Read these first", especially the terms and conditions, so you don't get banned.
Click the "API Keys" link, then "Create an App". Fill in the details; once validated, you will get a Key and a secret.
In the root of the project directory (at the same level that index.html is located), create a file called config.js
and add the following contents:
export default {
api_key: "YOUR_KEY_HERE"
}
The vue.js portfolio app I will build will display the images contained within a Flickr album. Browse our Flickr albums and find the one you wish to display. In the URL bar, copy the album ID and add it to the config file.

export default {
api_key: "YOUR_KEY_HERE",
photoset: "72157700194273182"
}
Making Our First API Call
We now have all the pieces to fetch photos from Flickr. Empty Home.vue
and replace it with this content.
<template>
<div class="home">
</div>
</template>
<script>
export default {
name: 'home',
};
</script>
Next, we will add some code to call the Flickr API using our key and show the results on the page. I'll explain each chunk of code and provide a complete listing at the end, which you can use to see how it fits together.
Firstly, we need some markup to show our page correctly.
<template>
<div>
<div class="wrapper" id="page">
<p v-if="loading">
Loading...
<ul v-else>
<li v-for="image in images" :key="image.id">{{image}}</li>
</ul>
</div>
</div>
</template>
Next, we must import our config file and the axios
package into the vue. This is done using the import
command followed by the module being imported and then the file it is imported from.
import config from '../../config';
import axios from 'axios';
Next, I'm going to define some data variables. One is to indicate if the page is busy (loading the contents), and the other variable is to hold the image data.
data() {
return {
loading: false,
images: []
}
},
Then, we add two functions: one that sets the page views up and another that fetches the images from Flickr.
methods: {
loadImages() {
this.loading = true;
this.fetchImages()
.then((response) => {
this.images = response.data.photoset.photo;
this.loading = false;
})
.catch((error) => {
console.log("An error ocurred: ", error);
})
},
fetchImages() {
return axios({
method: 'get',
url: 'https://api.flickr.com/services/rest',
params: {
method: 'flickr.photosets.getPhotos',
api_key: config.api_key,
photoset_id: config.photoset,
extras: 'url_n, url_o, owner_name, date_taken, views',
page: 1,
format: 'json',
nojsoncallback: 1,
per_page: 30,
}
})
},
},
The first method, loadImages
, is going to set the loading
variable to true. Through the magic of Vue data binding, when the loading
variable is true, the v-if
statement becomes true, and the loading text is shown else the list will be displayed. v-if
can be used on any element to show or hide conditionally. Next, this method will call our other method with a JavaScript promise. If it is successful, the images will be put into our data container. If it fails, the error will be logged into the console.
The second method uses Axios to query the Flickr API. It's relatively straightforward. You can see where we get the photoset ID and API key from the config; it will call the flickr.photosets.getPhotos
API method and return a few extra columns in the dataset.
The final thing we must do is get all this to show on the page load.
beforeMount(){
this.loadImages()
}
Putting all that together, we get the full contents of Home.Vue
.
<template>
<div>
<div class="wrapper" id="page">
<site-header />
<p v-if="loading">
Loading...
<ul v-else>
<li v-for="image in images" :key="image.id">{{image}}</li>
</ul>
</div>
</div>
</template>
<script>
import config from '../../config';
import axios from 'axios';
export default {
name: 'home',
components: { },
data() {
return {
loading: false,
images: []
}
},
methods: {
loadImages() {
this.loading = true;
this.fetchImages()
.then((response) => {
this.images = response.data.photoset.photo;
this.loading = false;
})
.catch((error) => {
console.log("An error ocurred: ", error);
})
},
fetchImages() {
return axios({
method: 'get',
url: 'https://api.flickr.com/services/rest',
params: {
method: 'flickr.photosets.getPhotos',
api_key: config.api_key,
photoset_id: config.photoset,
extras: 'url_n, url_o, owner_name, date_taken, views',
page: 1,
format: 'json',
nojsoncallback: 1,
per_page: 30,
}
})
},
},
beforeMount(){
this.loadImages()
}
};
</script>
Back in the PowerShell window, you can run the application using npm run serve
. You should see an ugly screen showing JSON data if all goes well.

The next thing to do is tidy this up and create a component or two to display the image nicely.
Make a Vue.js Component
Vue components are tiny, self-contained modules that you can reuse in a project. Components can be reused as many times as you want, which is lucky since we will build one component and reuse it multiple times.
In your src
folder, create a new folder called components
. Create a new file within this folder called ImageCard.vue
. Open this file in your editor.
The first thing we are going to add is the template markup.
<template>
<div class="item">
<a href="#">
<img class="image" :src="image.url_n" :alt="image.title">
<div class="body">
<p v-if="image.title" class="image-title">{{image.title}}
<p v-else class="image-title">No Title Found
<p class="image-owner">By {{image.ownername}}
<section class="image-date-view-wrapper">
<p class="image-date">{{image.datetaken}}
<p class="image-views">Views: {{image.views}}
</section>
</div>
</a>
</div>
</template>
You may have noticed the colon before an attribute; for example, I used :alt="image.title"
. The attribute is data bound to the image object's title property. For outputting data as plain text, we surround it in handlebars - {{image.title}}
.
Next, we add the TypeScript code. It's pretty simple here. We export a module named ImageCard
with a property called image.
<script>
import config from '../../config';
export default {
name: 'ImageCard',
props: [ 'image' ]
}
</script>
Next to add some styling so it looks pretty. Putting it all together, we get this.
<template>
<div class="item">
<a href="#">
<img class="image" :src="image.url_n" :alt="image.title">
<div class="body">
<p v-if="image.title" class="image-title">{{image.title}}
<p v-else class="image-title">No Title Found
<p class="image-owner">By {{image.ownername}}
<section class="image-date-view-wrapper">
<p class="image-date">{{image.datetaken }}
<p class="image-views">Views: {{image.views}}
</section>
</div>
</a>
</div>
</template>
<script>
import config from '../../config';
export default {
name: 'ImageCard',
props: [ 'image' ]
}
</script>
<style lang="scss">
.item {
background-color: #eee;
display: inline-block;
margin: 0 0 1em;
width: 100%;
}
.item a {text-decoration:none}
.item:hover .body {
visibility: visible;
opacity: 1;
}
.image {
width: 100%;
height: auto;
object-fit: cover;
}
.body {
padding: .5rem 1rem 1rem;
position:relative;
height:95px;
margin:-100px 0 0 0;
background:rgba(0,0,0,0.5);
color:white;
visibility: hidden;
opacity: 0;
transition: visibility 0s, opacity 0.5s linear;
}
.image-title {
font-weight: bold;
margin: 0;
}
.image-owner {
margin-top: 0;
font-size: .8rem;
}
.image-title,
.image-owner {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.image-date-view-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
}
.image-date,
.image-views {
margin-bottom: 0;
font-size: .8rem;
}
</style>
We need to make a few changes to the Home to use this component in our app.vue file.
First, we need to import the component.
import ImageCard from '@/components/ImageCard';
Next, in the components array, we need to include ImageCard so that it is available to use.
components: { ImageCard },
Next, we change the template markup to make use of the component. Instead of a list of raw image data, change it to this.
<p v-if="loading" class="text-centered">
Loading...
<div v-else class="masonry">
<image-card v-for="image in images" :key="image.id" :image="image" />
</div>
The property :image
we created in the component will be data-bound and contain the details from the image item in the loop. The loop will repeat the component, passing each image to the component. Then we can add some styling, and you should have this as your Home.vue
.
<template>
<div>
<div class="wrapper" id="page">
<p v-if="loading" class="text-centered">
Loading...
<div v-else class="masonry">
<image-card v-for="image in images" :key="image.id" :image="image" />
</div>
</div>
</div>
</template>
<script>
import config from '../../config';
import axios from 'axios';
import ImageCard from '@/components/ImageCard';
export default {
name: 'home',
components: {
ImageCard
},
data() {
return {
loading: false,
images: []
}
},
methods: {
loadImages() {
this.loading = true;
this.fetchImages()
.then((response) => {
this.images = response.data.photoset.photo;
this.loading = false;
})
.catch((error) => {
console.log("An error ocurred: ", error);
})
},
fetchImages() {
return axios({
method: 'get',
url: 'https://api.flickr.com/services/rest',
params: {
method: 'flickr.photosets.getPhotos',
api_key: config.api_key,
photoset_id: config.photoset,
extras: 'url_n, url_o, owner_name, date_taken, views',
page: 1,
format: 'json',
nojsoncallback: 1,
per_page: 30,
}
})
},
},
beforeMount(){
this.loadImages()
}
};
</script>
<style lang="scss">
.text-centered {
text-align: center;
}
.wrapper {
margin: 0 auto;
max-width: 1200px;
@media only screen and (max-width: 799px) {
max-width: 100%;
margin: 0 1.5rem;
}
}
.masonry {
margin: 1.5em auto;
max-width: 1200px;
column-gap: 1.5em;
}
@media only screen and (min-width: 1024px) {
.masonry {
column-count: 3;
}
}
/* Masonry on medium-sized screens */
@media only screen and (max-width: 1023px) and (min-width: 768px) {
.masonry {
column-count: 2;
}
}
/* Masonry on small screens */
@media only screen and (max-width: 767px) and (min-width: 540px) {
.masonry {
column-count: 1;
}
}
</style>
You should now have something like this in your browser. Your images will vary depending on the photoset you use.

Tiding Up Our App
There are a few things we can do to tidy up the app.
Sometimes, broken images come through from the API, so we can easily filter them out to not break the page.
This is done by adding a computed section at the end of the class.
...
beforeMount(){
this.loadImages()
},
computed: {
cleanImages() {
return this.images.filter(image => image.url_n)
}
},
};
We need to adjust the for loop to look at the filtered items.
<image-card v-for="image in cleanImages" :key="image.id" :image="image" />
Next, in the ImageCard.vue
, we can tidy up the dates using the moment library. This is done by creating a filter which will return formatted values.
export default {
name: 'ImageCard',
props: [ 'image' ],
filters: {
moment(date) {
return moment(date).format("Do MMMM YYYY");
}
}
}
In the template, make a quick change to use this filter. This will pass the value of image.datetaken
to the function moment
and output the result.
<p class="image-date">{{image.datetaken}}
Becomes
<p class="image-date">{{image.datetaken | moment}}
I also added a header to the page by creating a new component called SiteHeader.vue
with the following contents.
<template>
<div>
<header>
<a href="#"><h1>My Photography Portfolio</h1></a>
<div class="menu">
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
<li><a href="#">Blog</a></li>
<li><a href="#">Twitter</a></li>
</ul>
</div>
</header>
<h2 class="text-centered">Hi. I'm a Photographer</h2>
<p class="text-centered"> Add a catchy tagline or introduction here.
</div>
</template>
<script>
export default {
name: 'SiteHeader'
}
</script>
<style lang="scss">
header {
margin:1em 0 0 0;
}
header::after{
content: "";
clear: both;
display: table;
}
h1 {
width: 190px;
height: 72px;
background: url("../assets/logo.png") top right;
text-indent: -9999px;
margin: 0 auto;
}
.menu ul {
margin:1em 0 0 0;
padding:0;
}
.menu li {
width:100%;
padding:0.25em;
display:block;
list-style:none;
margin:0;
text-align:center;
}
.menu li a {
color:#777;
text-decoration:none;
text-transform:uppercase;
}
h2 {
margin:2em auto 1em auto;
}
p.text-centered {
color:#777;
margin-bottom:3em;
}
@media only screen and (min-width: 1024px) {
h1 {
margin:0;
float:left;
}
.menu {
float:right;
}
.menu li {
display:inline;
}
}
</style>
This is included in the Home.vue by adding an imports and markup tag.
import SiteHeader from '@/components/SiteHeader';
components: { SiteHeader, ImageCard },
<template>
<div>
<div class="wrapper" id="page">
<site-header />
<p v-if="loading" class="text-centered">
You should now have an excellent, pretty-looking portfolio website driven by a Flickr album using the Flickr API.

Conclusions and Download Sample Project
This has been a very quick introduction to Vue.js, including instructions on how to install Vue.js and create a new project using Axios to query the Flick API. We've seen how to build a component and use filters over a data set.
You can download the complete project source files using the link below.
Did you find this tutorial helpful? Was there anything missing or something you got stuck on? Let me know in the comments below, and I'll do my best to fill in the gaps.