Thoughts on Engineering, Photography, and Design.

Hey, I'm Ryan Heath. I design & develop things and play with cameras for fun. This is where I share my thoughts on all of that — and probably more — along the way.

Changing Views on AI

I’ll admit, I spent a long time criticizing AI tooling, perhaps a bit disappointed with what the future looked like. I know many people who were ecstatic about all of this, all-in from day one. But it always felt different for me. Part of my love for programming came from designing and writing beautiful code. Or at least, striving to design and write beautiful code. Most of my career I was reaching for that “craftsman” status. I believ(ed) in that. Code is creative. Code is art.

Then came AI. In the beginning it was laughable, zero threat. But it was clear where we were headed. As it got better and better, it began trying to rip that “craftsman” mindset away from me. It’s hard to pinpoint what I was bitter about, though. Was it because it (seemingly) “leveled the playing field” so all of the decades of hard work I had put into building software no longer mattered? Maybe. I realize that’s a goofy way to think, but I take a lot of pride in my work, and pride is funny like that.

But—sometime last year I moved through the stages of grief and started accepting my fate. This was the way. So at the beginning of 2026 I really embraced what it could do for me. I tried to think “AI-first” with most things I had to do. Lots of experiments, running local models, lots of reading, learning, and adapting. I was a tinkerer again.

The result? It kinda paid off. I have workflows now that I couldn’t imagine doing manually anymore.

I think my fears and dread all stemmed from AI replacing things. If I no longer designed the solutions to solve my problems, then what am I? Just a “prompter”? Blah. But that turned out to be so far from the truth, at least for me. What I realized very quickly is that an AI-assisted workflow provided peak productivity gains and still left me with all the control I wanted. And a bonus on top of it all: the better the engineer, the better the AI output. Which means experience, knowledge, preferences… all still matter.

While I’m still unsure how happy I am about the future, it’s at least a lot less bleak. Engineering is still about best practices, tradeoffs, and adapting to the ever-changing shifts in technology. This particular shift just took a lot of intentional effort and acceptance before I could thrive in the new world. But I’m here now, embracing it, keeping my fingers crossed along the way.

Querying APIs with Vanilla JS

I’m at the point in my career where keeping things simple is a top priority, even if that means a little sacrifice in other areas. In this particular case, as part of redesigning rpheath.com, I wanted to pull in the latest N posts from this blog. Given that I’m working with a static site, that meant using JavaScript. A young whippersnapper may immediately turn to React, Vue, Svelte, or (gasp!) Angular, which is reasonable. Those frameworks make this type of thing clean and easy. But I did not want to deal with integrating webpack and/or babel into my builds, as I’ve been down that road plenty, and it’s (shockingly!) still pretty annoying to deal with. Besides, each redesign of my personal site strives for more and more simplicity, so that would go against the grain.

I chose to use vanilla JS for this. Let’s talk about it! It all starts with a fetch-based function to grab the data from the blog’s API:

1function fetchPosts(url, callbackFn) {
2  fetch(url)
3    .then(response => response.json())
4    .then(data => callbackFn(data))
5    .catch(error => console.error(error));
6}

The fetchPosts() function grabs data from an API and hands it over to a callback function that I define for processing. This is all so simple because fetch() is built on the Promise, which in modern JavaScript, is an expectation for asynchronous calls.

Since we don’t have a framework to do stuff for us, the next “problem” we need to take care of is ensuring that our code runs at the right time. Meaning, after the DOM has loaded. Old school folks may remember jQuery’s $('document').ready(...) or $(function() { ... }) helpers! Yeah, it’s that, but with vanilla JavaScript. There are two ways to approach it:

 1// (1) wait for the DOMContentLoaded event
 2document.addEventListener("DOMContentLoaded", (_e) => {
 3  console.log("The DOM is ready!");
 4});
 5
 6// (2) a self-executing function
 7(function() {
 8  console.log("The DOM is ready!");
 9  console.warn("...but only if this JS is included at the bottom of the page");
10  console.warn("...also, I ignore deferred scripts!");
11})();

Both work, but notice the caveats with option (2) above. If you go with a self-executing function, it’ll execute whenever the browser reaches it, so it must be after your HTML is parsed, top-down (and even then it still ignores deferred scripts). We’ll go with option (1) for our purposes.

The next bit to work out is what to do with our data once we get it back from the API. In React, for example, we’d make a component that spit out some HTML for the browser. Well, that’s exactly what we’ll do, only we’ll use JavaScript’s Template Strings instead.

 1const renderPosts = (data, container) => {
 2  let posts = [];
 3
 4  data.forEach((post) => {
 5    const html = `
 6      <div class="post">
 7        <div class="post-date">
 8          <time>${post.date}</time>
 9        </div>
10        <div class="post-link">
11          <a href="${post.url}">${post.title}</a>
12        </div>
13        <p class="post-caption">${post.summary}</p>
14      </div>`;
15
16    posts.push(html)
17  });
18
19  container.innerHTML = posts.join("\n");
20};
21

I bet any youngsters who stumble onto this won’t even know what innerHTML is. But essentially, we just take the data and iterate over the response, creating an array of string-interpolated items. Once we’re done “looping”, we just set the container’s HTML to render our posts.

It’s a nice touch to put some kind of default text or loading indicator into your “container” element:

1<div id="blog-posts">
2  [spinner svg] Loading Posts...
3</div>

Then once the DOM loads and your function executes, innerHTML will replace your loading content with the interpolated response from the API.

Now that all of our pieces are in place, the result looks like this:

1document.addEventListener("DOMContentLoaded", (_e) => {
2  const blogContainer = document.querySelector("#blog-posts");
3
4  // we only want to do this for the "blog" section of the site
5  if (blogContainer) {
6    fetchPosts((response) => renderPosts(response.data, blogContainer));
7  }
8};

You can see this in action by going to rpheath.com/blog.

The important lesson here, if there is one, is to avoid defaulting to complexity because “that’s what the internet does”. If I wanted to use React for this, I totally could, and the outcome would look very clean and satisfying. But the complexity involved with integrating React into my builds was just not worth it. I have the same result, but with zero configuration or headache. I’m just using what browsers already know and understand. And for me, that’s the reward.

Setting Ownership in Rails

There’s a common pattern in a lot of applications to track who did what. Who created this post? Who was the last person to update it? Maybe it’s for accountability, maybe it’s because your client is nosy.

There are gems for this, but it’s such a simple thing, that extra dependency isn’t really worth it. So let’s take a look at a pretty simple way to achieve this in Rails 7 using a concern:

 1module Ownership
 2  extend ActiveSupport::Concern
 3
 4  included do
 5    before_validation :set_ownership
 6
 7    scope :created_by, ->(user_id) {
 8      where(creator_id: user_id)
 9    }
10    scope :updated_by, ->(user_id) {
11      where(updater_id: user_id)
12    }
13
14    belongs_to :creator, class_name: 'User',
15      foreign_key: 'creator_id', optional: true
16    belongs_to :updater, class_name: 'User',
17      foreign_key: 'updater_id'
18
19    validates :creator_id, :updater_id,
20      presence: true
21  end
22
23  private
24
25  def set_ownership
26    current_user = Current.user
27
28    if new_record?
29      self.creator_id = current_user.id
30      self.updater_id = current_user.id
31    else
32      self.updater_id = current_user.id
33    end
34  end
35end

Now, for any model that has creator_id and updater_id columns, you just have to include the Ownership module:

 1class Post < ApplicationRecord
 2  include Ownership
 3
 4  # ...
 5end
 6
 7>> @post = Post.find(1)
 8>> @post.creator
 9# => #<User id: 10 ... >
10>> @post.updater
11# => #<User id: 20 ... >

And whenever a Post is created, the creator is automagically set; similarly, whenever any Post is updated, the updater is automagically set. This, of course, assumes your application has user sessions, since you obviously need to know who’s logged in doing the creating and updating.