Pokémon Database App with Vue 3 Composition API and Script Setup Syntax

William Schulte
Vue.js Developers
Published in
10 min readDec 4, 2022

--

When I first upgraded from Vue 2 to Vue 3, one of the first tutorials I dove into was a Pokémon database app (built by Erik Hanchett) that utilizes the RESTful Pokémon API to retrieve Pokémon from a data library. This is, in my opinion, one of the best tutorials for kicking off Vue 3 projects after learning the basics of Composition API.

Since getting up to speed on the ins and outs of Vue 3, I’ve been curious as to how the code for this app would fare if refactored using Comp API with the <script setup> syntax. The focus of this article will therefore be on how to build a Pokémon database similar to Erik’s, while instead using the <script setup> syntax and (perhaps) slightly less code.

<script setup> Syntax…If You Don’t Already Know

Before starting the project, let’s learn a little about the <script setup> syntax. If you’ve been using Vue 3 over the past while, then you’re probably aware that Composition API utilizes a setup() function to encapsulate most of the functionality of a given component. Using an example in Erik’s original Pokémon database code, the functionality to return data for a selected Pokémon would look something like this:

<script>
import { reactive, toRefs } from "vue";
import { useRoute } from "vue-router";
export default {
setup() {
const route = useRoute();
const pokemon = reactive({
pokemon: null
});
fetch(`https://pokeapi.co/api/v2/pokemon/${route.params.slug}/`)
.then((res) => res.json())
.then((data) => {
pokemon.pokemon = data;
});
return { ...toRefs(pokemon) };
}
};
</script>

At some point after the Vue 3 rollout, The Vue team introduced the <script setup> syntax, allowing developers to further simplify their code blocks. Using the <script setup> syntax, the above code can be reduced to the following:

<script setup>
import { ref } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();
const pokemon = ref(null)

fetch(`https://pokeapi.co/api/v2/pokemon/${route.params.slug}/`)
.then((res) => res.json())
.then((data) => {
pokemon.value = data;
});
</script>

As you can see, using <script setup> allows us to implement functionality without explicitly using the setup() function and the export default wrapper. Also, observe that it is no longer necessary to handle a property return, as that functionality is handled behind the scenes.

Ultimately <script setup> is syntactic sugar for using Composition API inside Single-File Components (SFCs). The benefits of this syntax include:

  • More succinct code with less boilerplate (as seen in the above example)
  • Ability to declare props and emitted events using pure TypeScript (not applicable to this article, but still good to know!)
  • Better runtime performance (the template is compiled into a render function in the same scope, without an intermediate proxy)
  • Better IDE type-inference performance (less work for the language server to extract types from code)

One final note: <script setup> is now the recommended syntax if you are using both SFCs and Composition API, according to the docs. Keep that in mind!

Got it? Good. Time to get started!

Tailwind CSS Installation

Let’s kick off a new Vue 3 Tailwind CSS + Vite build. Run the following in the terminal:

npm init vite my-tailwind-project
cd my-tailwind-project

Next, install and initialize Tailwind CSS in your new project:

npm install -D tailwindcss
npx tailwindcss init -p

Add the following to tailwind.config.js:

module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

Create a new index.css file in ./src and add the following:

@tailwind base;
@tailwind components;
@tailwind utilities;

Update main.js to import index.css:

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

Vue Router Installation

Install the Vue Router package using npm install vue-router@4.

After installing router, create a new router folder in src/. Inside the folder, create a new router file called index.js.

Add the following to the index.js router file:

import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/Home.vue";

const routes = [
{
path: "/",
name: "Home",
component: Home
},
{
path: "/about/:slug",
name: "About",
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue")
}
];

const router = createRouter({
history: createWebHistory(),
routes,
})

export default router;

Update main.js to include the router file.

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './index.css'

createApp(App).use(router).mount('#app')

Building the App

After setting up the project, open up App.vue and replace the existing code with the following:

<template>
<div class="p-14 ">
<span class="flex justify-center text-4xl text-yellow-700">
Pokémon Picker&nbsp;
<img src="src/assets/800px-Poké_Ball_icon.png" class="w-10 h-10"/>
</span>
<router-view />
</div>
</template>

Add a folder called views to src in the project. Add two files to views called Home and About. The Home view will be the view that displays the input field to search for Pokémon. The About view will display data for the Pokémon selected in Home.

Home.vue

In the Home component, add the following template:

<template>
<div class="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-8">
<form class="mt-8 space-y-6" action="#" method="POST">
<input type="hidden" name="remember" value="true" />
<div class="-space-y-px rounded-md shadow-sm">
<div>
<input id="pokemon-name" v-model="text" class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm" placeholder="Enter Pokemon Here..." />
</div>
</div>

<div class="items-center">
<div
class="flex flex-wrap ml-4 text-2xl text-blue-400"
v-for="(pokemon, idx) in filteredPokemon"
:key="idx"
>
<router-link :to="`/about/${getPokemonId(pokemon.name)}`">
{{ pokemon.name }}
</router-link>
</div>
</div>
</form>
</div>
</div>
</template>

The above block will be used to display the input field, as well as a filtered list of Pokémon based on the user’s input.

Below the template, let’s build the functionality. First, we’ll use the built-in fetch() function to retrieve ALL 900 (or so) available Pokémon and return them to the log in your browser. You might need to comment out the references to filteredPokemon and getPokemon temporarily in the template in order to avoid errors.

(TBH, I didn’t know there were over 900 Pokémon until I started this project. I stopped watching Pokémon after 2001 so I’m still stuck on the first 150 🤷‍♂️)

<script setup>
import { computed, ref, onMounted } from "vue";
import { useRouter } from 'vue-router'

const router = useRouter()
const pokemons = ref([])
const text = ref("")

fetch('https://pokeapi.co/api/v2/pokemon?limit=100000&offset=0')
.then((res) => res.json())
.then((data) => {
pokemons.value = data.results
console.log(pokemons.value)
})
</script>

It looks like the fetch() function worked. We can now see all of the Pokémon available in the database!

Now let’s update the above code to include the filtering functionality:

<script setup>
import { computed, ref, onMounted } from "vue";
import { useRouter } from 'vue-router'

const router = useRouter()
const pokemons = ref([])
const text = ref("")

fetch('https://pokeapi.co/api/v2/pokemon?limit=100000&offset=0')
.then((res) => res.json())
.then((data) => {
pokemons.value = data.results
})

const getPokemonId = (item) => {
return pokemons.value.findIndex((p) => p.name === item) + 1;
};

const filteredPokemon = computed(() => {
if(!text.value){
return []
}
return pokemons.value.filter((pokemon)=>
pokemon.name.includes(text.value)
)
})
</script>

For the filtering functionality, we’ll use a computed property called filteredPokemon to return a filtered list of Pokémon based on the user’s input. Then we’ll use a getPokemonId function to return the name of the selected Pokémon based on the index of the selected Pokémon in the pokemons.value array + 1. The getPokemonId function is called by the selected router-link after passing in the name of the selected Pokémon as an argument.

The final code for Home.vue should look like this:

<template>
<div class="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-8">
<form class="mt-8 space-y-6" action="#" method="POST">
<input type="hidden" name="remember" value="true" />
<div class="-space-y-px rounded-md shadow-sm">
<div>
<input id="pokemon-name" v-model="text" class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm" placeholder="Enter Pokemon Here..." />
</div>
</div>

<div class="items-center">
<div
class="flex flex-wrap ml-4 text-2xl text-blue-400"
v-for="(pokemon, idx) in filteredPokemon"
:key="idx"
>
<router-link :to="`/about/${getPokemonId(pokemon.name)}`">
{{ pokemon.name }}
</router-link>
</div>
</div>
</form>
</div>
</div>
</template>

<script setup>
import { computed, ref, onMounted } from "vue";
import { useRouter } from 'vue-router'

const router = useRouter()
const pokemons = ref([])
const text = ref("")

fetch('https://pokeapi.co/api/v2/pokemon?limit=100000&offset=0')
.then((res) => res.json())
.then((data) => {
pokemons.value = data.results
})

const getPokemonId = (item) => {
return pokemons.value.findIndex((p) => p.name === item) + 1;
};

const filteredPokemon = computed(() => {
if(!text.value){
return []
}
return pokemons.value.filter((pokemon)=>
pokemon.name.includes(text.value)
)
})
</script>

About.vue

Add the following template code to About.vue, to display the profile for the user’s selected Pokémon:

<template>
<div class="about">
<div
v-if="pokemon"
className="w-3/12 m-auto bg-purple-100 mt-4 shadow-2xl flex justify-center flex-col items-center"
>
<h3 className="text-2xl text-green-900 uppercase">{{ pokemon.name }}</h3>
<div class="flex justify-center">
<img className="w-48" :src="pokemon.sprites.front_shiny" alt="" />
<img className="w-48" :src="pokemon.sprites.back_shiny" alt="" />
</div>
<h3 class="text-yellow-400">Types</h3>
<div v-for="(pkmn, idx) in pokemon.types" :key="idx">
<h5 class="text-blue-900">{{pkmn.type.name}}</h5>
</div>
</div>

<div class="p-14 flex justify-center">
<router-link to="/"
class="text-white
bg-purple-700
hover:bg-purple-800
focus:ring-4
focus:ring-blue-300
font-medium
rounded-lg
text-sm
px-5
py-2.5
text-center
mr-2
mb-2
dark:bg-purple-400
dark:hover:bg-purple-700
dark:focus:ring-purple-800">
Return to Picker
</router-link>
</div>
</div>
</template>

Below the template, add the following functionality:

<script setup>
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();
const pokemon = ref(null)
const isFound = ref(true)

fetch(`https://pokeapi.co/api/v2/pokemon/${route.params.slug}/`)
.then((res) => res.json())
.then((data) => {
pokemon.value = data;
});
</script>

The fetch() function in the above code is set up to fetch data for the selected Pokémon based on the Pokémon name passed to the route as a slug in Home.vue. The data retrieved for the selected Pokémon can then be populated in the template via the pokemon.value property.

This works fine. However, the batch returned from the API seems to also include a range of Pokémon data prone to errors. You can see examples of this when searching for Pokémon such as Pikachu or Charizard.

Clicking on these results will return errors.

I am still relatively new to this API and therefore do not know why entries such as “pikachu-rock-star” are included in the database. My recommendation in the meantime would be to implement error handling on the fetch() function to control the output when catching errors. To do this, we can simply wrap the fetch() function inside a try/catch block. If an error is caught, we’ll toggle the isFound.value property to false in order to display the “Pokémon Not Found” message in the template. Then, we’ll wrap the try/catch inside a new function called fetchPkmn(), while setting up an onMounted() hook to call fetchPkmn() when the page loads.

<script setup>
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();
const pokemon = ref(null)
const isFound = ref(true)

const fetchPkmn = async () => {
try {
await fetch(`https://pokeapi.co/api/v2/pokemon/${route.params.slug}/`)
.then((res) => res.json())
.then((data) => {
pokemon.value = data;
});
} catch (err) {
isFound.value = false
console.log(err)
}
}

onMounted(() => {
fetchPkmn()
})
</script>

The complete About.vue component (with the Pokemon Not Found message) should look like this:

<template>
<div class="about">
<div v-if="(isFound === false)" class="mx-auto mt-12 flex justify-center">
<span>Pokemon Not Found</span>
</div>

<div v-else>
<div
v-if="pokemon"
className="w-3/12 m-auto bg-purple-100 mt-4 shadow-2xl flex justify-center flex-col items-center"
>
<h3 className="text-2xl text-green-900 uppercase">{{ pokemon.name }}</h3>
<div class="flex justify-center">
<img className="w-48" :src="pokemon.sprites.front_shiny" alt="" />
<img className="w-48" :src="pokemon.sprites.back_shiny" alt="" />
</div>
<h3 class="text-yellow-400">Types</h3>
<div v-for="(pkmn, idx) in pokemon.types" :key="idx">
<h5 class="text-blue-900">{{pkmn.type.name}}</h5>
</div>
</div>
</div>

<div class="p-14 flex justify-center">
<router-link to="/"
class="text-white
bg-purple-700
hover:bg-purple-800
focus:ring-4
focus:ring-blue-300
font-medium
rounded-lg
text-sm
px-5
py-2.5
text-center
mr-2
mb-2
dark:bg-purple-400
dark:hover:bg-purple-700
dark:focus:ring-purple-800">
Return to Picker
</router-link>
</div>
</div>
</template>

<script setup>
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();
const pokemon = ref(null)
const isFound = ref(true)

const fetchPkmn = async () => {
try {
await fetch(`https://pokeapi.co/api/v2/pokemon/${route.params.slug}/`)
.then((res) => res.json())
.then((data) => {
pokemon.value = data;
});
} catch (err) {
isFound.value = false
console.log(err)
}
}

onMounted(() => {
fetchPkmn()
})
</script>

After running the app again, search for Pikachu and click on “pikachu-rock-star”. See if the error-handling worked.

And there it is! You’ve now learned how to build the Pokémon database app with Vue, Composition API and <script setup> syntax! Congratulations!

Click here to see the Github repo for this project:

https://github.com/jsfanatik/pokemon-vue-script-setup

Stay tuned for more tutorials on the horizon!

A Caterpie May Change Into A Butterfree, But The Heart That Beats Inside Remains The Same.” — Brock, Pewter City Gym Leader

--

--

William Schulte
Vue.js Developers

Mobile App Developer, Coding Educator, LDS, Retro-gamer 🎮