Or How to make your own simple CORS enabled API from one that has CORS disabled
As a front-end developer, I often consume various 3rd party APIs while developing. These APIs could be for weather, crypto currency prices or the latest XKCD comic.
The problem with some of them is that they do not support cross-origin requests (CORS), which means that client-side AJAX calls to these services do not work. This is frustrating but can easily be fixed with the help of a few lines of code in your own micro-service.
Creating the micro-service
All you need to create a simple micro-service is a package called micro. It is a very simple package that enables the creation of asynchronous micro-services, and if you read the project’s readme, you’ll see that a simple service can be created with no more than a few lines:
module.exports = (req, res) => {
res.end(‘Hello world’)
}
Obviously the above is completely useless, but let me show how easy it is to consume almost any API, at least any API that didn’t need authentication, using micro.
From comic #1810
For this example, I’ll use the free API from the XKCD comic, which cannot be used in AJAX calls on the client-side because CORS is disabled. Calling the url https://xkcd.com/info.0.json returns the latest comic, like so:
{
"month": "2",
"num": 1954,
"link": "",
"year": "2018",
"news": "",
"safe_title": "Impostor Syndrome",
"transcript": "",
"alt": "It's actually worst in people who study the DunningâKruger effect. We tried to organize a conference on it, but the only people who would agree to give the keynote were random undergrads.",
"img": "https://imgs.xkcd.com/comics/impostor_syndrome.png",
"title": "Impostor Syndrome",
"day": "12"
}
And it can return a specific comic if passed the correct comic ID (https://xkcd.com/1500/info.0.json):
{
"month": "3",
"num": 1500,
"link": "",
"year": "2015",
"news": "",
"safe_title": "Upside-Down Map",
"transcript": "((A mercator projection of the world map is shown. All the continents have been rotated one hundred eighty degrees.))\n\n((Cuba is next to alaska, and alaska is touching the tip of south america, which is all near the equator. Mexico is now friends with greenland.\n\n((Iceland, the UK, and asia are all close together. Japan and Taiwan haven't moved with the asian continent, and are technically European.))\n\n((Siberia is now equatorial. Africa is pretty temperate, except for the north bits which are somewhat antarctic.))\n\nCaption: This upside-down map will change your perspective on the world!\n\n{{Title text: Due to their proximity across the channel, there's long been tension between North Korea and the United Kingdom of Great Britain and Southern Ireland.}}",
"alt": "Due to their proximity across the channel, there's long been tension between North Korea and the United Kingdom of Great Britain and Southern Ireland.",
"img": "https://imgs.xkcd.com/comics/upside_down_map.png",
"title": "Upside-Down Map",
"day": "18"
}
All the micro-service needs to do is to pass-through any requests to the original API and set a few headers to allow for cross-origin requests so that they can be used in AJAX calls on the client-side, like so:
const axios = require('axios')
const { send } = require('micro')
const microCors = require('micro-cors')
const cors = microCors({ allowMethods: ['GET'] })
const DOMAIN = 'https://xkcd.com/'
const handler = async function(req, res) {
const params = req.url
const path = `${DOMAIN}${params}`
const response = await axios(path)
send(res, 200, response.data)
}
module.exports = cors(handler)
That’s 14 lines of code!
The example above passes any slug information into the API (e.g. 1000/0.json
), so calling https://xkcd.now.sh/1000/0.json
(my version of the API), would map to https://xkcd.com/1000/0.json
. This could be the end of our journey, but I’d like to improve the API UX a bit by changing the endpoints:
-
xkcd.now.sh
should return the latest comic -
xkcd.now.sh/1000
should return comic ID 1000
See below on how to achieve that:
const axios = require('axios')
const { send } = require('micro')
const microCors = require('micro-cors')
const cors = microCors({ allowMethods: ['GET'] })
const DOMAIN = 'https://xkcd.com/'
const PATH = 'info.0.json'
const handler = async function(req, res) {
let id = req.url.replace('/', '')
const comicId = id ? `${id}/` : ''
const path = `${DOMAIN}${comicId}${PATH}`
const response = await axios(path)
id = response.data.num
let newResponse
if (id >= 1084) {
newResponse = {
...response.data,
imgRetina: `${response.data.img.replace('.png', '')}_2x.png`,
}
} else {
newResponse = {
...response.data,
}
}
send(res, 200, newResponse)
}
module.exports = cors(handler)
That’s 29 lines of code! See it here 👀
You can see above that beyond micro, there are two other packages that the service relies on:
- axios for the HTTP requests
- micro-cors is a simple CORS for micro
My example with the XKCD API returns all of the original data and actually changes the response data slightly as well as how the API is consumed. I decided to add the retina image path (if there is one) as well as simplify the call to the API. So instead of calling xkcd.com/1894/info.0.json
you can call xkcd.now.sh/1894
.
So, for example: calling https://xkcd.now.sh/1894 would request this URL from the original XKCD API: https://xkcd.com/1894/info.0.json.
{
"month": "9",
"num": 1894,
"link": "",
"year": "2017",
"news": "",
"safe_title": "Real Estate",
"transcript": "",
"alt": "I tried converting the prices into pizzas, to put it in more familiar terms, and it just became a hard-to-think-about number of pizzas.",
"img": "https://imgs.xkcd.com/comics/real_estate.png",
"title": "Real Estate",
"day": "25",
"imgRetina": "https://imgs.xkcd.com/comics/real_estate_2x.png"
}
💪 The code for this service is hosted on GitHub at github.com/mrmartineau/xkcd-api and can be tested using Postman here.
Hosting your new API
I use now by zeit to host my various apps and APIs. now supports the JavaScript language features that this micro-service requires (async/await) as well as HTTPS out of the box. If your hosting does not support these features, you will need to transpile the code back to a version it does support.
From comic #1700
Other examples
For an example of an even more simple pass-through API, you can see my CORS enabled version of the Pinboard feeds API. The code is hosted on GitHub at github.com/mrmartineau/pinboard-api
My thanks go to Andrew Williams, Ashley Nolan & Ciaran Park for their help with the title of this post. Other suggestions from them include:
- No CORS for concern: getting that API
- Be-CORS you're worth it
- COR Blimey gov’nr
- CORS, Ugh, what is is it good for
- Just CORS
Top comments (4)
FIY, the example deployment has moved from xkcdapi.now.sh to xkcd.now.sh as of today.
Do you have a working example of filtering by origin with deployments on Now ? It seemed to me it was not reflected because of their container proxy or some other reason, but I'll have to check again.
Xkcdapi.now.sh was a mistake, I have updated the post to use the correct URL: XKCD.now.sh
Can you clarify what you mean by "filtering by origin"?
In the case of a web app, if the static front-end is served from a domain name example.com, you'd probably want your backend to allow CORS only for requests coming from that origin.
I know it's not the same use case as your example, which is adding an open CORS on top of a public API, but I was wondering how that would work.
Let's say we want to make a GraphQL API for XKCD, but have it work only with our own front-end (for some reason).
We'd publish the server that queries the XKCD API and exposes a GraphQL endpoint on Now, but allowing CORS only from whatever domain name we've bound to our front-end static server (could also be Now, or Surge, or whatever static hosting service there is out there).
Thank you, It is really really helpful