When building web applications, there are many occasions when you might need to access and display data via an API. There are several ways to do so, but a trendy approach is to use Axios, a promise-based HTTP client.
There are many ways to request information via the API. But it's best to first determine the data's format to decide what to display. So we will make a call at the API endpoint to get this information and then output it to our browser.
First, we would install Axios through a CDN.
Then we will create a data object that will eventually hold our information. Next, we'll retrieve it and assign it using the lifecycle hook.
Starting Our Project
With Vue.js, you can build an app around one of these services and start serving content to users in minutes.
To follow, you'll need Node.js and Yarn installed. You have two options to install Node: either go to the official download page to grab the Node binaries, or you can use a version manager.
Once Node is set up, pull in Yarn.
npm i -g yarn
Purchase An API Key
We will be using the NYTIMES API. Therefore, an API key is required to use the NYTimes API. If you don't have an API key, go to their signup page—register to get one for the Top Stories API.
We'll use the top stories API endpoint to fetch data. This API allows you to fetch data from multiple sections like "home," "travel," "arts," or "science." We will create a filter so users can select the section they want and load the stories.
You can test API calls using your favorite REST client (such As Hoppscotch, Insomnia).
Structuring Your Project
Create a Vue 3 project with Vite. Vite is faster than VueCLI and can be used to spin up dev servers.
yarn create @vitejs/app vue-news-app --template vue
# Install package dependencies
cd vue-news-app
yarn install
# Confirm app can run
yarn dev
Open localhost:3000 in your browser.
Now install the TailwindCSS framework for styling. This action will require you to stop the server.
yarn add -D tailwindcss@latest postcss@latest autoprefixer@latest
# Generate tailwind.config.js and postcss.config.js files
npx tailwindcss init –p
It would help if you also had additional package utilities to clamp the lines and format dates:
yarn add @tailwindcss/line-clamp date-fns
Below is the full configuration of tailwindcss:
module.exports = {
purge: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [require("@tailwindcss/line-clamp")],
}
Now create an "Index.css" in the folder "src" and write this code.
module.exports = {
purge: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [require("@tailwindcss/line-clamp")],
}
To help us determine the default theme of our app, we have also imported the Tailwind CSS classes. A flex layout system has been implemented to create a sticky header/footer for our application.
Import index.css in src/main.js
import { createApp } from "vue"
import App from "./App.vue"
import "./index.css"
createApp(App).mount("#app")
Now create the application layout. First, clear the components in src/components and create these files in the same folder.
Observe the following code and copy for every single file:
src/components/Footer.vue:
<template>
<footer>
class="px-4 py-8 text-sm font-bold text-center text-green-100 bg-green-900">
<p class="text-sm tracking-wide">Copyright (c) 2021 SitePoint</p>
</footer>
</template>
src/components/Header.vue:
<template>
<header class="flex justify-center py-6 bg-green-900 place-items-center">
<img alt="Vue logo" src="../assets/logo.png" width="32" />
<span class="ml-4 text-lg font-bold text-green-100 md:text-xl">
Vue News | NYTimes Edition
</span>
</header>
</template>
src/components/Layout.vue:
<template>
<Header />
<main class="container flex-grow px-4 mx-auto my-12">
<slot />
</main>
<Footer />
</template>
<script>
import Header from "./Header.vue"
import Footer from "./Footer.vue"
export default {
components: {
Header,
Footer,
},
}
</script>
Now update src/App.vue:
<template>
<Layout>
<p>Main content goes here</p>
</Layout>
</template>
<script>
import Layout from "./components/Layout.vue"
export default {
components: {
Layout,
},
}
</script>
Execute the yarn dev, and the browser will restart automatically.
Once the app layout has been completed, we can begin building the core logic of the news app.
News App Components
The first step is to create the layout and search for the components. To begin, we will need mock data. Create src/posts.json and infuse the following data:
{
"posts": [
{
"title": "Stay Healthy When Exercising Outdoors",
"abstract": "Cold weather workouts do bring unique risks, but a little planning and preparation can help whether you’re going for a winter walk, trekking in snowshoes or sledding with the kids.",
"url": "https://www.nytimes.com/2021/02/06/at-home/exercise-outdoors-cold-weather.html",
"byline": "By Kelly DiNardo",
"published_date": "2021-02-06T23:40:05-05:00",
"thumbnail": "https://static01.nyt.com/images/2021/02/07/multimedia/07ah-OUTDOOREXERCISE/07ah-OUTDOOREXERCISE-mediumThreeByTwo210.jpg",
"caption": ""
},
{
"title": "4 Skiers Killed in Avalanche in Utah, Officials Say",
"abstract": "It was the third such deadly episode in days and the deadliest avalanche in the United States since May 2014, according to the authorities.",
"URL": "https://www.nytimes.com/2021/02/06/us/avalanche-salt-lake-city.html",
"byline": "By Michael Levenson",
"published_date": "2021-02-06T20:22:39-05:00",
"thumbnail": "https://static01.nyt.com/images/2021/02/06/lens/06xp-avalanche-photo2/06xp-avalanche-photo2-mediumThreeByTwo210.jpg",
"caption": "A helicopter returning to Millcreek Canyon after rescuing one of the four avalanche survivors on Saturday."
}
]
}
You are welcome to make duplicates of the records to test the component design layouts. However, space constraints can prevent you from doing that here.
Let's build the News components.
Navigate to the components folder and create the following files:
● NewsCard.vue
● NewsList.vue
● NewsFilter.vue
Then import them in src/App.vue and set them out as follows to visualize them for yourself:
<template>
<Layout>
<h2 class="mb-8 text-4xl font-bold text-center capitalize">
News Section : <span class="text-green-700">{{ section }}</span>
</h2>
<NewsFilter v-model="section" />
<NewsList :posts="posts" />
</Layout>
</template>
<script>
import Layout from "./components/Layout.vue"
import NewsFilter from "./components/NewsFilter.vue"
import NewsList from "./components/NewsList.vue"
import data from "./posts.json"
export default {
components: {
Layout,
NewsFilter,
NewsList,
},
data() {
return {
section: "home",
posts: data.posts,
}
},
}
</script>
Let's begin to work with each News component. The NewsCard.vue component will fetch the data for a single post and will require one prop:
<template>
<section class="p-4 rounded-lg shadow-lg bg-gray-50 w-80">
<div class="h-96">
<a
class="text-xl font-bold text-center text-green-800 hover:text-green-600 hover:underline"
:href="post.url"
target="_blank"
rel="noreferrer"
>
{{ post.title }}
</a>
<img
class="w-full mt-2 rounded"
:src="post.thumbnail"
:alt="post.caption"
height="140"
width="210"
/>
<p class="mt-2 text-justify text-gray-700 line-clamp-4">
{{ post.abstract }}
</p>
</div>
<div>
<p class="mt-4 font-bold text-gray-600">{{ post.byline }}</p>
<p class="font-light text-gray-600">
{{ formatDate(post.published_date) }}
</p>
</div>
</section>
</template>
<script>
import { format } from "date-fns"
export default {
props: {
post: {
type: Object,
required: true,
},
},
methods: {
formatDate(strDate) {
return format(new Date(strDate), "MMMM do, yyyy")
},
},
}
</script>
The NewsList.vue will run through a posts array, populating NewsCards across the Grid:
<template>
<div
class="grid grid-cols-1 gap-6 mt-4 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 justify-items-center"
>
<NewsCard v-for="(post, index) in posts" :key="index" :post="post" />
</div>
</template>
<script>
import NewsCard from "./NewsCard.vue"
export default {
props: {
posts: {
type: Array,
required: true,
},
},
components: {
NewsCard,
},
}
</script>
Next, we will use the NewsFilter component to allow users to load any post from different sections. To store all sections supported by the Top Stories API endpoint, first, create a content folder.
Create the following file src/components/sections.js:
Observe:
<template>
<div
class="grid grid-cols-1 gap-6 mt-4 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 justify-items-center"
>
<NewsCard v-for="(post, index) in posts" :key="index" :post="post" />
</div>
</template>
<script>
import NewsCard from "./NewsCard.vue"
export default {
props: {
posts: {
type: Array,
required: true,
},
},
components: {
NewsCard,
},
}
</script>
Now create the NewsFilter.vue. To link the state section to App.vue we'll use v-model
<template>
<div class="flex justify-center p-4 rounded">
<!-- Start of select dropdown -->
<div class="relative inline-flex">
<svg
class="absolute top-0 right-0 w-2 h-2 m-4 pointer-events-none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 412 232"
>
<path
d="M206 171.144L42.678 7.822c-9.763-9.763-25.592-9.763-35.355 0-9.763 9.764-9.763 25.592 0 35.355l181 181c4.88 4.882 11.279 7.323 17.677 7.323s12.796-2.441 17.678-7.322l181-181c9.763-9.764 9.763-25.592 0-35.355-9.763-9.763-25.592-9.763-35.355 0L206 171.144z"
fill="#648299"
fill-rule="nonzero"
/>
</svg>
<select
class="h-10 pl-5 pr-10 text-gray-600 bg-white border border-gray-300 rounded-lg appearance-none hover:border-gray-400 focus:outline-none"
v-model="section"
>
<option
v-for="(section, index) in sections"
:key="index"
:value="section"
>
{{ capitalize(section) }}
</option>
</select>
</div>
<!-- End of select dropdown -->
<div class="self-center ml-8">
<button
class="px-6 py-2 text-white bg-green-700 rounded hover:bg-green-900"
>
Retrieve
</button>
</div>
</div>
</template>
<script>
import { computed } from "vue"
import sectionsData from "./sections"
export default {
props: {
modelValue: String,
},
setup(props, { emit }) {
const section = computed({
get: () => props.modelValue,
set: value => emit("update:modelValue", value),
})
return {
section,
}
},
data() {
return {
sections: sectionsData,
}
},
methods: {
capitalize(value) {
if (!value) return ""
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
},
},
}
</script>
This allows child components to update the props and sync up with their parent component.
Fetching Remote Data With Axios
Axios is a promise-based HTTP client for executing Ajax requests and provides an easy and rich API. It's very similar to the fetch API. However, it doesn't require you to add a Polyfill for older browsers and other subtleties.
To install Axios, follow these steps:
yarn add Axios
Now we can move on from the UI development. What we need only is the remote fetching logic.
First, let us save our API keys in a .env file located at the root directory of our project. This is how to save:
VITE_NYT_API_KEY=####
You can replace the hashes in this section with your API key.
Let's now create the logic that will retrieve posts from NYTimes API endpoint. Simply update src/App.vue as following:
<template>
<Layout>
<h2 class="mb-8 text-4xl font-bold text-center capitalize">
News Section : <span class="text-green-700">{{ section }}</span>
</h2>
<NewsFilter v-model="section" :fetch="fetchNews" />
<NewsList :posts="posts" />
</Layout>
</template>
<script>
import Layout from "./components/Layout.vue"
import NewsFilter from "./components/NewsFilter.vue"
import NewsList from "./components/NewsList.vue"
import axios from "axios"
const api = import.meta.env.VITE_NYT_API_KEY
export default {
components: {
Layout,
NewsFilter,
NewsList,
},
data() {
return {
section: "home",
posts: [],
}
},
methods: {
// Helper function for extracting a nested image object
extractImage(post) {
const defaultImg = {
url: "http://placehold.it/210x140?text=N/A",
caption: post.title,
}
if (!post.multimedia) {
return defaultImg
}
let imgObj = post.multimedia.find(
media => media.format === "mediumThreeByTwo210"
)
return imgObj ? imgObj : defaultImg
},
async fetchNews() {
try {
const url = `https://api.nytimes.com/svc/topstories/v2/${this.section}.json?api-key=${api}`
const response = await axios.get(url)
const results = response.data.results
this.posts = results.map(post => ({
title: post.title,
abstract: post.abstract,
url: post.url,
thumbnail: this.extractImage(post).url,
caption: this.extractImage(post).caption,
byline: post.byline,
published_date: post.published_date,
}))
} catch (err) {
if (err.response) {
// client received an error response (5xx, 4xx)
console.log("Server Error:", err)
} else if (err.request) {
// client never received a response, or request never left
console.log("Network Error:", err)
} else {
console.log("Client Error:", err)
}
}
},
},
mounted() {
this.fetchNews()
},
}
</script>
Create the function fetchNews that will contain logic for performing the fetch.
Let's look at each function in detail to help us understand the basics.
● Because it is cleaner than regular Promise callback syntax, we are using async syntax.
● Many things could go wrong as we prepare to make a call over the network. That is why we've wrapped the function's code within a try...catch block. If we don't do that, users will receive a Promise error that is non-descriptive if this occurs.
● Using ES6 literals, we can create a URL string that is automatically updated when the news section is modified via the NewsFilter module. You will also see the API key in the URL string.
● After fetching results with the Axios.get()(), it's time to parse and format the results in a manner compatible with our UI. We will use JavaScript's Array.map function to create a new array with our data.
● It is not easy to extract image data. For example, some posts don't have a multimedia field, and there's no guarantee that the required multimedia format is there. In such cases, we return a default URL image and use the title as the caption.
● To determine what type of error occurred, we check specific error properties in the error block. You can use this information for a useful error message.
Now take a look at the template section. You will see that fetch has been added. This link takes you to the fetchNews function. You will, however, need to update src/components/NewsFilter.vue to accept the prop.
To load the Axios libraries and API keys properly, you'll need to restart the dev server. You should now be able to see actual posts once you have done this.
Summary
In this tutorial, we have learned to create a Vue.js project and fetch data from an API using Axios.
We have also learned how to handle responses and manipulate data using components.
With a Vue.js.3.0 application, we can improve user experience in many ways. For example:
● Queuing posts from a category using our Buffer API
● We can use the Pocket API to save some posts to be read later
Contact us now if you need help with importing data to your website to improve user experience.