How to Use APIs in a Web Game (Pokémon Example with JSON Optimization)
How I Built a Pokémon API Game and Optimized It Using JSON
I built a simple browser game where you compare two Pokémon and guess which one has higher stats. The idea is very simple. You see two Pokémon and pick the one you think has the higher value. The game uses real data from an API, in this case the PokéAPI. That makes it a good small project to understand how APIs work in a real scenario.
Overview
- Random Pokémon Generator
- Fetching Data per Round
- Using a Local JSON File
- Performance Impact
- Conclusion
At the beginning I used the most direct approach. Every time the user clicked a button, the game sent a fetch request to the API. I had already used this pattern in an earlier project, a random Pokémon generator, so it felt natural to reuse it. For that kind of tool it works fine.
The problem started when I turned the idea into a game. In a higher or lower game you do not just fetch once. You fetch multiple times per round. One round needs two Pokémon. Ten rounds can easily mean twenty requests. And one player will usually play more than one session. That quickly increases the number of requests.
This is a common mistake when working with APIs. Just because you can fetch data on every interaction does not mean it is a good idea. Many APIs have rate limits, require tokens, or even cost money. Even if an API is free, it is still better practice to reduce unnecessary requests and not depend on an external service for every single action.
I chose the PokéAPI because it is open, free to use, and does not require authentication. That makes it perfect for learning and experimenting. But even with a free API, the scaling problem is still there.
In this article I will show how I built the game, what problems came up when using live API requests, and how I fixed them by switching to a local JSON file. This also gives a clear picture of how APIs are used in real projects, not just in theory but in a way that actually works when more users are involved.
First Project: Random Pokémon Generator
Before building the higher or lower game, I had already worked on a smaller project: a random Pokémon generator. This project is very simple. You click a button, and it shows you a random Pokémon with its name and sprite.
The way this works is by using the fetch method to request data from the PokéAPI. A random ID is generated, and then the script sends a request to the API using that ID. From the response, it only takes the data that is actually needed for this project, which is the Pokémon’s name and the sprite image. That data is then displayed on the page.
This approach is simple and works well for small tools like this. One click means one request, and the user gets a result immediately. For a project like a random generator, that is completely fine.
I also wrote a separate blog post where I go into more detail about how this generator works, and all the code for that project is available as well. You can find the links here:
Try the Random Pokémon Generator
The random generator is an older project that I made a while ago, and I decided to build something new on top of that idea. I wanted to make a small game, something simple and quick to implement. A higher or lower game was an obvious choice.
So I reused parts of the same logic, especially the fetch-based approach. But once I started building the game, some problems started to appear, which I will explain in the next section.
First Approach: Fetching Data per Round
When I started building the higher or lower game, I first used the same approach as before. The game logic was simple. You click a button to start, the game fetches two random Pokémon from the API, stores their name, sprite URL, and stats, and then calculates the total stats. After that, you choose which Pokémon you think has the higher value. Then the next round starts and the same process repeats.
At first, this works fine. But the problem becomes clear when you look at how many requests are actually made. One round already needs two API calls. If a player plays ten rounds, that is twenty requests. And most players will play more than one session. This means that even a single user can generate a lot of requests in a short time.
Now scale that up. If multiple users are playing at the same time, the number of requests increases very quickly. You can easily reach hundreds or thousands of requests per hour or per day. Even though the PokéAPI is free and does not require authentication, it still encourages developers to avoid unnecessary requests. Sending large amounts of repeated requests from the same website is not good practice and can lead to problems, including being rate limited or blocked.
There are some partial solutions to this. One option would be to cache data in the browser. That means if a Pokémon was already fetched once, you store it and reuse it instead of fetching it again. This can reduce some requests per user. However, with a dataset of over 1000 Pokémon, the chance of hitting the same Pokémon again is relatively low, especially in short sessions. So while caching helps a bit, it does not really solve the core problem.
The main issue is that every round still depends on live API requests. As long as that is the case, the number of requests will always scale with the number of users.
Because of that, I needed a different approach. The solution is actually simple. Instead of fetching data during gameplay, I moved all the data into a local JSON file. That way, the game only loads the data once and runs entirely in the browser without making repeated API calls.
In the next section, I will show how I built that JSON file and how it is used in the game.
Solution: Local JSON File
To solve this problem, I changed the approach completely. Instead of fetching data during gameplay, I wrote a separate script that fetches all the required data once and stores it locally.
This script goes through every Pokémon and fetches the relevant data from the API. That means I make over 1000 requests, but only once during the build process, not during gameplay. From each response, I only keep the data that I actually need. This includes the ID, the name, the sprite URL, and the stats. The total stats are calculated directly in the script by summing up all the individual stats.
One important detail here is how the images are handled. The JSON file does not store the images themselves, only the URL to the sprite. This keeps the file very small and efficient. If you were to store actual image data inside the JSON file, it would become extremely large and slow. Using URLs avoids that problem completely.
After collecting all the data, everything is stored in a single JSON file. This file now acts as the data source for the game. Instead of calling the API during gameplay, the game simply loads this JSON file once and then works with that data.
This makes a big difference. The game no longer depends on live API requests, which means it runs faster and more reliably. There is no waiting time for network requests during each round. The logic becomes very simple as well. Instead of fetching a Pokémon, the game just selects a random entry from the JSON data and uses that.
Of course, using a local JSON file also has some trade-offs. The data is no longer live. If new Pokémon are added or existing data changes, the JSON file has to be rebuilt. This can be done manually, or you can automate it. For example, if you use GitHub, you can set up an action that rebuilds the JSON file at a fixed interval.
For this specific project, this is not really a problem. Pokémon data does not change often, and new Pokémon are only added occasionally. Updating the data once in a while is more than enough. For other types of projects, like a weather app, this approach would not work because the data needs to be updated constantly.
In this case, using a local JSON file is clearly the better solution. It reduces the number of API requests to zero during gameplay, improves performance, and gives full control over the data. It also makes it easy to extend the game later by adding more fields, such as individual stats, generation filters, or special categories like legendary or final evolution Pokémon.
Performance and Why This Matters in Practice
One immediate improvement from this approach is performance. When the game was still using live API requests, every round depended on network speed. Even if the API responds quickly, there is always a delay. The browser has to send the request, wait for the response, and then process the data.
This delay is small, but it adds up. In a game where you go through multiple rounds, it becomes noticeable. The game feels less responsive because every new round depends on external data.
With the JSON approach, this problem disappears. The data is loaded once at the beginning, and after that everything runs locally in the browser. Selecting a random Pokémon is instant. There are no delays between rounds caused by network requests.
This also makes the game more stable. If the API is slow or temporarily unavailable, the game would stop working in the old version. With local data, the game works independently from the API after the initial load.
Another advantage is consistency. Every player gets the same data and the same performance. There are no differences caused by network conditions or API response times.
This shows an important principle when working with APIs. Live requests are useful, but they should be used carefully. If the data does not need to change constantly, it is often better to load it once and reuse it instead of requesting it over and over again.
Conclusion
One nice side effect of this approach is that it makes the system very easy to extend. Since all the data is already stored locally, you are not limited to just one use case. Right now, the game only compares total stats, but you could easily expand this.
For example, you could store individual stats like HP, attack, defense, or speed and let the player choose what to compare. You could also add filters, such as only showing Pokémon from a specific generation, only legendary Pokémon, or only final evolutions. All of this is just additional data in the JSON file and a bit of logic in the game. The structure stays the same.
This is much harder to do with live API requests, because every new feature would require more requests and more complex logic. With local data, you already have everything available, and you can build on top of it without worrying about performance or request limits.
At the same time, this approach is not always the right choice. It works well here because Pokémon data does not change often. If you are building something like a weather app, stock tracker, or anything that depends on real-time data, you cannot rely on a static JSON file. In those cases, live API requests are necessary.
For this project, however, using a local JSON file is clearly the better option. It reduces complexity during gameplay, improves performance, and makes the system easier to expand in the future.
Try the project
Play the Pokémon Higher or Lower Game
Other Related projects
Disclaimer: This project is not affiliated with Pokémon, Nintendo, Game Freak, or any related companies. It is a personal project created for educational purposes only.
Comments
Post a Comment