diff --git a/src/routes/posts/2024.05.27.adding-single-click-dev-environment-links-to-your-open-sourc.md b/src/routes/posts/2024.05.27.adding-single-click-dev-environment-links-to-your-open-sourc.md index 6b2e768..d5b99a9 100644 --- a/src/routes/posts/2024.05.27.adding-single-click-dev-environment-links-to-your-open-sourc.md +++ b/src/routes/posts/2024.05.27.adding-single-click-dev-environment-links-to-your-open-sourc.md @@ -2,7 +2,6 @@ title: 'Adding single click dev environment links to your open source project for easy contributions' date: 2024-05-27T05:10:06Z description: 'See how you can streamline contributions and onboarding by leveraging DevContainers and cloud dev environments - a one-click setup that lets new folks dive right in!' -highlighted: true --- [I have been using DevContainers](/2023.02.10.why-use-devcontainer/) a lot. If diff --git a/src/routes/posts/2024.10.06.i-built-an-api-for-random-names.md b/src/routes/posts/2024.10.06.i-built-an-api-for-random-names.md new file mode 100644 index 0000000..e1ae20a --- /dev/null +++ b/src/routes/posts/2024.10.06.i-built-an-api-for-random-names.md @@ -0,0 +1,178 @@ +--- +title: 'I built an API for random names and numbers because why not?' +date: 2024-10-06T14:18:00Z +description: 'So I needed random names for a script, and instead of using a sensible solution, I decided to build an entire API. Join me as I fumble through Go, preact, and Kamal, then end up with a functional free public API.' +highlighted: true +--- + +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](https://www.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](https://gofiber.io/). I primarily +looked up [a benchmark for requests per second](https://web-frameworks-benchmark.netlify.app/result?asc=0&l=go&metric=totalRequestsPerS&order_by=level64), +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](https://pkg.go.dev/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](https://github.com/samber/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](https://preactjs.com/). preact itself is okay, not much to talk +about. It's basically react. But one thing I immediately fell in love with was +[signals](https://preactjs.com/guide/v10/signals/). It's a beautiful solution, +allowing you to handle both global and local state with ease. + +Then I put that together with [TailwindCSS](https://tailwindcss.com/) and +[DaisyUI](https://daisyui.com/). These are my go-to picks nowadays, it makes +building a decent looking interface really quickly. The whole thing is then +bundled with [Vite](https://vite.dev/). + +## 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. + +```yml +# 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. + +```json +{ + "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](https://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.