DEV Community

Zander Martineau
Zander Martineau

Posted on • Updated on • Originally published at Medium

Rebel with a CORS

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)
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

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"
}
Enter fullscreen mode Exit fullscreen mode

💪 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)

Collapse
 
franky47 profile image
François Best

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.

Collapse
 
mrmartineau profile image
Zander Martineau

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"?

Collapse
 
franky47 profile image
François Best

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).

Collapse
 
harshav33985081 profile image
HarshaVardhan N

Thank you, It is really really helpful