A few weeks ago, I was go­ing through my Vercel pro­jects when I found a pro­ject I made 4 years ago. It was so old it still used the now.sh do­main when Vercel used to be ZEIT. It was a proxy for the xkcd API be­cause the ac­tual API had CORS is­sues on the browser.

Cross-origin re­source shar­ing

Cross-origin re­source shar­ing (CORS) is a mech­a­nism that al­lows re­stricted re­sources on a web page to be ac­cessed from an­other do­main out­side the do­main from which the first re­source was served.
[Wikipedia] - [MDN]

According to my Vercel dash­board, it was used more than I thought it’d be, with 12,000 calls to the API be­tween September 11 and January 27.

xkcd API usage analytics on my Vercel dashboard

However, af­ter 4 years, the code was a bit of a mess with du­pli­cate files from try­ing out dif­fer­ent server­less providers. The API URLs were also a bit too long which was not to my lik­ing, and it was in dire need of refac­tor­ing.

Couldn't decide on which cloud provider


The ini­tial code (written in JavaScript) was pretty straight­for­ward.

const fetch = require('node-fetch');

module.exports = async (request, response) => {
const { num } = request.query;

// in retrospect
// this was a bit dumb
// !num would also return true for `num == ""`
// num == " " would never happen either
if (!num || num == "" || num == " " || num == "latest") {
let req = await fetch("http://xkcd.com/info.0.json");
let res = await req.json();
response.status(200).json(res);
} else {
let req = await fetch(`http://xkcd.com/${num}/info.0.json`);
let res = await req.json();
response.status(200).json(res);
}
}

I’d been want­ing to prac­tice my Go skills for a while, and this was the per­fect op­por­tu­nity be­cause Vercel’s Serverless Functions sup­ported Go, among other lan­guages.

At the time of writ­ing, the Go run­time is in Beta.

the es­sen­tials

To use Go with Vercel, cre­ate a folder named api with all your rel­e­vant Go code in­side that folder. By de­fault, Vercel will serve them at yourapp.vercel.app/api/filename (which we will change).

Alternatively, you can spec­ify what files you want Vercel to treat as Serverless Functions by con­fig­ur­ing the functions prop­erty in your vercel.json.

// vercel.json

{
"functions": {
"api/test.js": {
"memory": 3008, // optional config
"maxDuration": 30 // optional config
},
"api/*.js": {
"memory": 3008,
"maxDuration": 30
}
}
}

get­ting started

We start it like any other Go pro­ject.

$ go mod init vercel-functions-demo

This cre­ates a go.mod file that you can use to spec­ify your de­pen­den­cies and more im­por­tantly, the Go ver­sion that you want Vercel to use. Vercel de­faults to Go v1.20.

writ­ing go code

The xkcd API re­quired two routes - one to fetch the lat­est comic and an­other one to fetch a spe­cific comic by its num­ber. To make it back­ward-com­pat­i­ble with my pre­vi­ous API be­cause I am a con­sid­er­ate de­vel­oper who cares about his users’ feel­ings, I de­cided to have two dif­fer­ent files for both of these routes. While I could per­form all the magic in one file, it would­n’t be con­sid­er­ate to me as my mas­sive air-de­prived brain would not be able to han­dle it.

Ignoring the non-es­sen­tial files which will later be­come es­sen­tial, this is what the file struc­ture looks like.

.
├── api/
│ ├── latest.go
│ └── number.go
├── go.mod
├── vercel.json
└── ...

long be­fore time be­gan
there was the `func Handler` #

All great things have to start some­where. While my code is­n’t that great, it has to start some­where re­gard­less.

package handler

import (
"fmt"
"net/http"
)

func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<h1>Hello from Go!</h1>")
}

If you’re one of those old peo­ple who use JavaScript like me, the above code is sim­i­lar to the fol­low­ing.

export default function handler(request, response) {
const { name = 'World' } = request.query;
return response.send(`Hello ${name}!`);
}

First, let’s do the one to fetch the lat­est xkcd comic, aptly named latest.go.

// latest.go

package handler

import (
"fmt"
"io"
"log"
"net/http"
)

func LatestComic(w http.ResponseWriter, r *http.Request) {
...
}

While I was work­ing on this, I no­ticed dif­fer­ent sources set­ting a WriteHeader be­fore send­ing the re­sponse. However, it re­sulted in the re­sponse be­ing sent as plain text rather than JSON even though I had set the right Content-Type.

If the code works, don’t ques­tion it.

I don’t know if this is a law, but if it is­n’t, I’d like to claim it.

func LatestComic(w http.ResponseWriter, r *http.Request) {

// contrary to what you might find online,
// do not send a status code
// because it sends the data as text/plain
// idk why
// w.WriteHeader(http.StatusCreated)

// set Content-Type so that we send JSON
w.Header().Set("Content-Type", "application/json")
}

Onto the fetch­ing now!

  // get the latest xkcd comic
resp, err := http.Get("https://xkcd.com/info.0.json")
if err != nil {
log.Fatalln(err)
}

// make it readable or something i really don't know
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalln(err)
}

And then SEND IT.

  if err != nil {
fmt.Println("Error happened somewhere along the way. Err:", err)
} else {
// 200 send it lads
w.Write(body)
}

Putting it all to­gether, it looks like this:

that was easy

Now onto the slightly more dif­fi­cult part. For the spe­cific comic API route, we have a few con­di­tions. The code should ex­tract the comic num­ber from the URL if it is in ei­ther of these forms:

// extract comic number from path
comic := r.URL.Path[1:]

// reverse compatibility with old API
// /api/comic?num=1481
queryParams := r.URL.Query().Get("num")
if queryParams != "" {
comic = queryParams
}

Reading this code now I re­al­ize that if some­one sent a re­quest to /1481?num=1337, 1337 would take prece­dence over 1481. However, it’s the user’s fault for not read­ing the doc­u­men­ta­tion.

ver­cel.json (very im­por­tant!!1!1!!)

This is the most im­por­tant part of the whole thing. It’s more im­por­tant than any­thing you’ve ever con­sid­ered im­por­tant in your life. Without the cus­tom redi­rects that we need to spec­ify in vercel.json, our API would­n’t be all slick and cool.

We also need to en­able CORS - the whole rea­son why we’re here in the first place.

// vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Access-Control-Allow-Credentials", "value": "true" },
{ "key": "Access-Control-Allow-Origin", "value": "*" },
{
"key": "Access-Control-Allow-Methods",
"value": "GET"
},
{
"key": "Access-Control-Allow-Headers",
"value": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
}
]
}
],
}

I hope you un­der­stand the im­pli­ca­tions of CORS. Normally, you would­n’t have re­sources such as APIs ac­ces­si­ble from ex­ter­nal do­mains, but in this case, we need it to re­ceive re­quests from every­one and every­where.

We uti­lize some Vercel magic called rewrites to serve our API at short URLs. This is a pretty sweet fea­ture be­cause I can show dif­fer­ent pages for dif­fer­ent routes and even dif­fer­ent re­quest head­ers. I used this to make it so that if some­one di­rectly vis­its the root URL with an Accept header of application/json, the lat­est comic de­tails will be re­turned. If the header is ab­sent, a nice land­ing page with the API doc­u­men­ta­tion is dis­played.

It’s im­por­tant to note that rewrites are dif­fer­ent from redirects as the for­mer di­rectly serves the spec­i­fied con­tent for a par­tic­u­lar route, whereas the lat­ter sends a 308 Permanent Redirect (or 307 Temporary Redirect) to the spec­i­fied con­tent.

// vercel.json

{
"rewrites": [
// rewrite / to /api/latest
// if it has an Accept `application/json`
{
"source": "/",
"destination": "/api/latest",
"has": [
{
"type": "header",
"key": "Accept",
"value": "application/json"
}
]
},

// if the header is missing
// send the html page instead
// NOTE: i used `app.html` instead of `index.html`
// because file names take priority
// over rewrites
{
"source": "/",
"destination": "/app.html",
"missing": [
{
"type": "header",
"key": "Accept",
"value": "application/json"
}
]
},

// /latest and /0 to the latest comic
{
"source": "/latest",
"destination": "/api/latest"
},
{
"source": "/0",
"destination": "/api/latest"
},

// the `source` property also supports regular expressions
// here, \d{1,} matches a number
// https://regexr.com/7s75m
{
"source": "/:number(\\d{1,})",
"destination": "/api/number"
},

// compatible with
// /api/comic?num=latest
{
"source": "/api/comic",
"has": [
{
"type": "query",
"key": "num",
"value": "latest"
}
],
"destination": "/api/latest"
},

// compatible with
// /api/comic?num=1481
{
"source": "/api/comic",
"has": [
{
"type": "query",
"key": "num",
"value": "(\\d{1,})"
}
],
"destination": "/api/number"
}
]
}

Finally, the com­plete vercel.json.

con­clu­sion

Live - https://​getxkcd.ver­cel.app/
GitHub - https://​github.com/​khal­by786/​getxkcd

Deploy with Vercel

getxkcd.vercel.app screenshot

Vercel’s Serverless Functions are re­ally pow­er­ful pieces of in­fra­struc­ture (is that the right word?) that you can use to host nifty util­ity APIs like this one. I said pow­er­ful be­cause you can do more than just send JSON data. Vercel sup­ports a va­ri­ety of data­base op­tions in­clud­ing a Redis cache which can be handy in caching ex­ter­nal re­sponses.

getxkcd stats

ad­den­dum

xkcd 1481 - API

ACCESS LIMITS: Clients may maintain connections to the server for no more than 86,400 seconds per day. If you need additional time, you may contact IERS to file a request for up to one additional second.