Today, we are going to play around with an API from recipepuppies.com which isn't a perfect API - the intention is to learn how to navigate problems you may come across with APIs. Hopefully, you will learn to work around issues or know when you should walk away from the API for your project.
I am creating this post as notes from Wes Bos Javascript course which you can sign up for and do with me here: https://beginnerjavascript.com/. Here is a link to Wes' GitHub readme.md.
Recipe Puppy API
Recipe Puppy has a very simple API. This api lets you search through recipe puppy database of over a million recipes by keyword and/or by search query. We only ask that you link back to Recipe Puppy and let them know if you are going to perform more than 1,000 requests a day (we shouldn't be).
The api is accessible at http://www.recipepuppy.com/api/.
For example: http://www.recipepuppy.com/api/?i=onions,garlic&q=omelet&p=3
You can see in the above URL path ?i=onions,garlic&q=omelet&p=3
which is a query parameter (also known as a query string). This is a simple method browsers can communicate search criterion with the server.
If we break down this Query Parameter:
? // Starts the query
i=onions,garlic
& // separates query items
q=omelet
&
p=3
Optional Parameters:
i : comma delimited ingredients
q : normal search query
p : page
format=xml : if you want xml instead of json
As usual we are going to start with an HTML document inside of our project file, use this template as we are focusing on Javascript for this exercise
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>Recipe Fetcher</title> <link rel="stylesheet" href="base.css"> <style> .search { display: grid; grid-template-columns: 1fr; } button[disabled] { opacity: 0.2; } .recipes { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-gap: 20px; } .recipe { border: 1px solid rgba(0, 0, 0, 0.1); padding: 20px; } </style> </head> <body> <div class="wrapper"> <form class="search" autocomplete="off"> <input type="text" name="query" value="pizza"> <button name="submit" type="submit" disabled>Submit</button> </form> <div class="recipes"></div> </div> <script src="scripts.js"></script> </body> </html>
Note: the button is set to disabled by default.
You can download the base.css file here: https://github.com/wesbos/beginner-javascript/blob/master/base.css and place it inside the project folder.
Lastly, we want to create a file and name it scripts.js
and create our basic fetch script. If you haven't already - I covered how to get this in my last article on APIs here.
const baseEndpoint = 'http://www.recipepuppy.com/api/'; async function fetchRecipes(query){ const res = await fetch(`${baseEndpoint}?q=${query}`); const data = await res.json(); console.log(data); }; fetchRecipes('pizza');
If you run this right now in your browser you will get back an ugly error that reads something like this:
Access to fetch at 'http://www.recipepuppy.com/api/?q=pizza' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
CORS stands for Cross Origin Resource Shares - this is a policy written on the origin server that prevents sharing data unless the origins are the same (codingwithdrew.com to codingwithdrew.com for example) but if you try to share data across two different domains like codingwithdrew.com and github.com. By default, websites can't just use other site's data without permissions. You can dive in more of the details here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS.
Often times running the site on a server will resolve the CORS error. From terminal, open the project folder and run npm init -y
then navigate to your newly created package.json file and edit it to look like mine:
{ "name": "CORSandRecipes", "version": "1.0.0", "description": "", "main": "scripts.js", "scripts": { "start": "parcel index.html" }, "keywords": [], "author": "", "license": "ISC" }
Back in that terminal run: npm install parcel-bundler
to install parcel. It'll take a moment so take this time to turn on some good tunes. You will see the parcel bundler in the package.json file as a dependency like this:
"dependencies": { "parcel-bundler": "^1.12.4"}
After parcel finishes installing, run npm start
and a localhost:1234 (or similar url) path will be visible. You can now open that URL path in a browser and the CORS error will be gone!
Ope, there is a new error!
Uncaught ReferenceError: regeneratorRuntime is not defined
This has nothing to do with CORS, although we will return to CORS in just a moment. The issue here is called by babel (a dependency of parcel which translates your ES6 javascript to something like ES4 javascript. So we need to tell it not to translate async
& await
. We can get around this by editing our package.json.
{ "name": "CORSandRecipes", "version": "1.0.0", "description": "", "main": "scripts.js", "scripts": { "start": "parcel index.html" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "parcel-bundler": "^1.12.4" }, "browserslist": ["last 1 chrome versions"] }
This tricks babel into thinking that you are running the latest version of chrome and there is no reason to translate it. You may need to delete your .dist
and .cache
folders that were created and then use ⌘C
to kill the currently running parcel bundler and then run it again.
The CORS error is back. The issue is that we have a localhost trying to connect to recipepuppy - the solution is to use something called a proxy. You could build that yourself or in some cases, when not sharing private data, you could use something called a cors proxy. The one we are going to use is https://cors-anywhere.herokuapp.com/
This is not something you would want to do if you are using any kind of sensitive application on, you can however easily use this for learning and educational purposes. For applications that would use username, passwords, PII, or the like, you'd need to create your own server for this.
All we have to do is pop that URL infront of our api URL in our fetch like this:
const baseEndpoint = 'http://www.recipepuppy.com/api/'; async function fetchRecipes(query){ const res = await fetch(`https://cors-anywhere.herokuapp.com/${baseEndpoint}?q=${query}`); const data = await res.json(); console.log(data); }; fetchRecipes('pizza');
And just like that, we have defeated this CORS error! WOooooHoooooOOOO!
Refactoring:
We can use a variable in place of the proxy URL like we did with the baseEndpoint. The next thing we want to do is make the form button submit a query to the API.
const baseEndpoint = 'http://www.recipepuppy.com/api/'; const proxy = 'https://cors-anywhere.herokuapp.com/'; const form = document.querySelector('form.search'); async function fetchRecipes(query){ const res = await fetch(`${proxy}${baseEndpoint}?q=${query}`); const data = await res.json(); console.log(data); return data; }; function handleSubmit(event){ event.preventDefault(); console.log(event.currentTarget.query.value); }; form.addEventListener('submit', handleSubmit); fetchRecipes('pizza');
We can create some async await functionality to force the button to disable momentarily while the API is fetched.
const baseEndpoint = 'http://www.recipepuppy.com/api/'; const proxy = 'https://cors-anywhere.herokuapp.com/'; const form = document.querySelector('form.search'); async function fetchRecipes(query){ const res = await fetch(`${proxy}${baseEndpoint}?q=${query}`); const data = await res.json(); console.log(data); return data; }; async function handleSubmit(event){ event.preventDefault(); console.log(event.currentTarget.query.value); const btn = event.currentTarget; btn.submit.disabled = true; //submit the search const recipes = await fetchRecipes(form.query.value); console.log(recipes); btn.submit.disabled = false; }; form.addEventListener('submit', handleSubmit); fetchRecipes('pizza');
Now you should be able to display anything you'd like from that recipe puppy API. Make an effort to use the API to display results upon clicking submit. If you get stuck you can review my results below! No cheating!
Final Thoughts
APIs really unlock the power of an interconnected web and allows us to access big data - I hope that this tutorial was not only fun but informative. Overcoming CORS errors can be difficult! There are some really fun APIs out there for learning. A personal favorite of mine: https://rickandmortyapi.com/. You can play around with it and make your very own rick and morty app if you wanted to. Below is the final results from this project.
const baseEndpoint = 'http://www.recipepuppy.com/api/'; const proxy = 'https://cors-anywhere.herokuapp.com/'; const form = document.querySelector('form.search'); const recipesGrid = document.querySelector('.recipes'); async function fetchRecipes(query){ const res = await fetch(`${proxy}${baseEndpoint}?q=${query}`); const data = await res.json(); console.log(data); return data; }; async function handleSubmit(event){ event.preventDefault(); console.log(event.currentTarget.query.value); const btn = event.currentTarget; btn.submit.disabled = true; //submit the search const recipes = await fetchRecipes(form.query.value); console.log(recipes); btn.submit.disabled = false; displayRecipes(recipes.results); }; function displayRecipes(recipes){ console.log('creating HTML'); console.log(recipes); const html = recipes.map( recipe => `<div class="recipe"> <h2>${recipe.title}</h2> <p>${recipe.ingredients}</p> ${recipe.thumbnail && `<img src="${recipe.thumbnail}" alt ="${recipe.title}">`} </div>` ); recipesGrid.innerHTML = html.join(''); }; form.addEventListener('submit', handleSubmit); fetchRecipes('pizza');
If you found this article helpful, share/retweet it and follow me on twitter @codingwithdrewk! There is so much more in Wes' courses I think you will find valuable as I have. I'm learning so much and really enjoying the course, Wes has an amazingly simple way to explain the difficult bits of Javascript that other courses I've taken could only wish. You can view the course over at WesBos.com. (I am in no way getting any referrals or kickbacks for recommending this)
Drew is a seasoned DevOps Engineer with a rich background that spans multiple industries and technologies. With foundational training as a Nuclear Engineer in the US Navy, Drew brings a meticulous approach to operational efficiency and reliability. His expertise lies in cloud migration strategies, CI/CD automation, and Kubernetes orchestration. Known for a keen focus on facts and correctness, Drew is proficient in a range of programming languages including Bash and JavaScript. His diverse experiences, from serving in the military to working in the corporate world, have equipped him with a comprehensive worldview and a knack for creative problem-solving. Drew advocates for streamlined, fact-based approaches in both code and business, making him a reliable authority in the tech industry.