I built an API for random names and numbers because why not?
Written on
I was recently writing up a script for… something. I honestly can’t quite
remember anymore, but I needed to generate random names in that script. Think
something like how Github will assign random names to Codespaces like upgraded giggle
.
You can always just call openssl rand -hex 12
and get a random string,
but memorable phrases are always better.
It might not be everyday that you need to read out a container name or remember
a URL off the top of your head, but when you do it helps out so much to have a
phrase than a random sequence of letters and numbers.
Importantly for this case, I was writing a basic script that couldn’t assume much about what is installed or present on the system, and I didn’t want to pull in any extra files or bloat the script with a large dictionary of words. So I thought it would be a smart idea to call a public API to get a random name instead!
Simple public APIs like ipify.org are always a life saver when you’re writing up a small script and you just need something real quick. But I couldn’t find one for generating random words or names. So I decided to take this up and build one myself.
Building the API
I’ve been wanting to learn Go for a while, partly because my new job uses Go in some parts. Nothing I work on right now, but I thought it would be useful in case something did come up. And besides, I’ve been looking for something in-between NodeJS and Rust in the tradeoff of developer experience and performance, and thought Go might strike a good balance.
I ended up building the service with Fiber. I primarily looked up a benchmark for requests per second, and Fiber stood out as the top option that is actively maintained and well documented.
Playing around with that benchmark, you can see there are other frameworks that excel in other tasks, but I primarily care about requests per second since that is going to ultimately decide how much load the API can handle, and stuff like response latency is not particularly important.
So how was Go?
Building the API took very little time. I am extremely impressed with Go in that
regard. Sure, the constant if err != nil
checks do get tedious, but unlike
when I was using Rust I never had to grapple with incomprehensible compiler
errors. That’s definitely an unfair comparison for Rust, and there are of course
cases where Go’s garbage collector is going to be an unacceptable tradeoff. But
building APIs is my use case for these languages.
I was pleasantly surprised with the standard library embed functionality, which allows you to bundle resources into the program itself. I think this is the way to go for all small projects where the backend/API is serving the UI. You can always put a caching proxy/CDN in front of the backend, and it simplifies your deployments since you now always have a matching version of the UI and backend combined bundled together into a single file or container.
The main thing I didn’t like was the error handling. I think returning errors
from functions is a good call, but requiring explicit checks for them makes the
code noisy. I wish there was an equivalent of Rust’s ?
operator that allows
you to easily bubble up errors, which is what you often want. In an API, it’s
very natural to handle errors at the API level, so you can always bubble the
error up and then turn it into a 400 or 500 response at a higher level. Or if
there was support for compiler macros, so someone could build this support into
Go.
I did find the amazing Mo, which brings Monads
to Go. With support for Option
and Result
, it does everything I want and
more. I didn’t end up really using it, but I could see that being a better
option than passing nil
s around.
And that is one thing I was really disappointed with Go, nil
. At least make it
strongly typed so it’s obvious what is nullable and what is not!
The docs
For documenting the API, I decided to build something myself. I knew I wanted an interactive doc, and felt that I could have some fun with it. So I built a site using preact. preact itself is okay, not much to talk about. It’s basically react. But one thing I immediately fell in love with was signals. It’s a beautiful solution, allowing you to handle both global and local state with ease.
Then I put that together with TailwindCSS and DaisyUI. These are my go-to picks nowadays, it makes building a decent looking interface really quickly. The whole thing is then bundled with Vite.
Deploying
Deployment was an interesting question too. In the past I have usually done it manually. I also dabbled with ansible a bit, but never used it to deploy stuff. But I have gotten really used to automated deployments at work, so I wanted something similar for this project. At the same time, I didn’t want to just throw the project up on a serverless hosting solution, because I’m building a free public API here and I don’t want to wake up to a million-dollar bill one day.
Thanks to some lovely feedback I got on the Fediverse, I found out about Kamal though.
Setting up Kamal was in some ways easier than I expected. It did take me a few attempts of fiddling with CI and Tailscale, but I eventually got it working.
I have Tailscale running on the small VPS hosting the API. I use the Tailscale Github Action to temporarily authenticate the CI runner with Tailscale.
# In a Github Actions workflow:
# ...
- name: Tailscale
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
- name: Deploy
run: |
gem install kamal
kamal deploy
I enabled Tailscale SSH on the VPS, then configured the access controls so the CI runners can authenticate with the VPS without messing around with SSH keys.
{
"tagOwners": {
// Temporary CI runners
"tag:ci": ["autogroup:admin"],
// Devices that can receive deploys from CI
"tag:target": ["autogroup:admin"],
},
"acls": [
// ...
// CI has access to CI targets
{
"action": "accept",
"src": ["tag:ci"],
"dst": ["tag:target:*"],
},
],
"ssh": [
{
"action": "accept",
"src": ["tag:ci"],
"dst": ["tag:target"],
"users": ["root"],
},
],
}
And that was about it. After following the instructions, Kamal deployed everything and even handled getting an SSL certificate for me. This API is now up and running at rnd.bgenc.dev, which is a new domain I recently bought for any small projects I build like this.
As a side note, I now own bgenc.com
, .net
, and .dev
! I’m really excited
because someone was squatting bgenc.com
for a while, but it finally became
available a couple months ago and I grabbed it instantly. I wasn’t even
checking, but I got an email for some scam offering to help me get .com and .org
because they were becoming available soon. I checked the domain and tada, it in
fact was abandoned. After refreshing the domain registry’s page for days, I
grabbed it as soon as I could.