Coding with Jesse

A rabbit hole in 5 commits

looking up out of a hole Photo by Mr Xerty on Unsplash

When I'm working with a client, I respect their time and energy by keeping things simple and easy for them. I make as many decisions as I can, and try not to bog them down with every complex decision I have to make.

I've learned to trust my instincts, and to escalate important choices only when the trade-offs are truly business decisions.

This is a story of when this instinct led me in the wrong direction.

I often work on projects with fixed-price and flexible-scope. What this means is my clients know exactly what a project will cost, but I'm also okay with the odd surprise.

That day, I was working on a web app that was powered by static data files. I was working on the simplest task of the project. The client wanted to add a link so users can download all the data files in a zip file. Sounds straightforward, right?

Commit #1: "NEW: add data download"

<a href="data.zip">Download Data</a>

I could have just zipped the data files up on my laptop and called it a day. However, I didn't want to be in a situation where I forgot to update the zip file if the data changed. I decided to automate the process by creating the zip file during the automated deployment pipeline.

Commit #2: "NEW: create zip file in deployment pipeline"

"scripts": {
"build": "npm run build:download; ...",
"build:download": "cd public/static/data; rm -f data.zip; zip *.csv data.zip"
},

I pushed that change and the pipeline failed immediately.

sh: 1: zip: not found

Seriously? I looked at the pipeline configuration in bitbucket-pipelines.yml. It was running off a seven-year-old container that didn't have zip installed.

I could have made a new, custom container that had everything we needed. I decided that would give the project another thing to manage over the long run, which I thought it'd be best to avoid. Instead, I thought I'd simplify things by using the official node.js container.

I added a line to the pipeline to install rsync (to upload files to the staging server) and zip. It would only add a few seconds to the pipeline, but then we wouldn't need to maintain a custom container.

Commit #3: "FIX: use latest node.js container, install rsync and zip"

image: node:latest
pipelines:
  branches:
    main:
      - step:
          script:
            - apt-get update
            - apt-get install -y rsync zip

The pipeline failed again.

> npm run build:download; webpack --mode=production --env.router hash --env.appPublicPath public --config ./production.config.js

Error in bail mode: Error: callback(): The callback was already called.
    at context.callback (/opt/atlassian/pipelines/agent/build/node_modules/loader-runner/lib/LoaderRunner.js:106:10)
    at processTicksAndRejections (node:internal/process/task_queues:103:5) Error: callback(): The callback was already called.
    at context.callback (/opt/atlassian/pipelines/agent/build/node_modules/loader-runner/lib/LoaderRunner.js:106:10)
    at processTicksAndRejections (node:internal/process/task_queues:103:5)

What the heck?

The web app was being built with Webpack v4, a very old build tool. Turns out Webpack v4 breaks on newer versions of node.js.

I decided to try using an older node.js container. After some experimentation, I found that Webpack v4 needed node.js v16 or older. Of course, once I had the right version of node, the build in the container still failed!

E: The repository 'http://deb.debian.org/debian buster Release' does not have a Release file.
E: The repository 'http://deb.debian.org/debian-security buster/updates Release' does not have a Release file.
E: The repository 'http://deb.debian.org/debian buster-updates Release' does not have a Release file.

This time it was because this old node docker container is built on an old and outdated version of Linux. The operating system is so old that I couldn't even install rsync and zip on it without some serious workarounds.

If you come to a fork in the road, take it

At this point I knew I was going in the wrong direction. Getting old versions to work together wasn't the simple & quick solution I had hoped it was. I wasn't going to spend any more time getting five-year-old software to play nice together. The best solution was to modernize this legacy project by replacing Webpack with Vite.

My client had surely never heard of Webpack or Vite, and they certainly would never have asked me to make the switch. I knew it was way outside the scope of this project. I also knew it would take me way longer than I estimated.

But more than all of that, I knew it was the right solution for this moment, and for the project in the long term. If I didn't deal with this properly today, the system would remain fragile, and only get worse. More problems like these would surely arise in the future, and I didn't want that burden and risk to persist.

I decided to push forward.

Commit #4: "FIX: replace Webpack with Vite"

Lines updated +2778 -8272

This was the deepest part of the rabbit hole, but I was happy to finally be rid of Webpack. The project was much better off for it, no longer stuck on outdated versions and tools. I was bracing myself for a huge task, but it didn't take as long as I feared, and I was very happy with the result.

Finally, I was able to use the latest versions of everything in the deployment pipeline, and it worked!

Delivering the download button

After all this, I sent my client an email and let them know the download button was in place. I explained that the zip file was being built automatically in the deployment pipeline. Best of all, I had modernized the pipeline so it was running the latest versions of everything and would even be a bit faster.

The client thanked me and asked, will the zip file update automatically when they upload new data files?

Damn, of course not. The deployment pipeline is used during development only, when I push changes to the staging server. Once the web application was live, I had assumed the client would create zip files manually whenever they uploaded new data files. It was a bad assumption on my end, and of course I was happy to deliver a better solution that works well for my client.

Commit #5: "NEW: use jszip to generate the zip file in the browser"

async function download() {
  // import jszip only when needed
  const JSZip = (await import('jszip')).default;
  const zip = new JSZip();

  for (const filename of CSV_FILES) {
    zip.file(filename, await fetchFile(filename));
  }

  const zipBlob = await zip.generateAsync({ type: 'blob' });
  const url = URL.createObjectURL(zipBlob);
  const link = document.createElement('a');
  link.href = url;
  link.download = 'data.zip';
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  URL.revokeObjectURL(url);
}

I deleted all the zip stuff from the pipeline, but of course I kept the modernization. I switched to creating the zip file on the fly in the browser using the jszip library. The download button was working great.

Simplifying things for my clients

When given a problem, I'm always looking to deliver the simplest solution. I'm willing to do the hard work to keep things simple for my clients, and for the users of the software.

At the start, having a static zip file seemed like the simplest solution. In the end, the best solution was the one that worked best for my client, and I was happy to deliver that instead.

By keeping things simple for my clients, I took on a lot of complexity. I went further down this rabbit hole than I had to, but I'm glad this legacy web application ended up being modernized in the process.

This easy task ended up taking way longer than I had expected, but my client never needed to know that. My client was very happy with the result. From their perspective, their project had been modernized, but I still finished everything on budget and on time.

Published on March 1st, 2026. © Jesse Skinner

Unpivoting

In 1989, when I was eight, I started learning Logo and Basic programming on my Coleco Adam. I was absolutely fascinated to realize those video games like Buck Rogers were made with the same programming tools I was learning! I wanted to learn how to make computers do things like that.

In 1998, when I was seventeen, I had my first job at a movie theatre. On my break, I would fantasize about starting a web design company. I wondered if I could charge $10 per page. Maybe that was too much to ask?

In 2001, when I was twenty, I got my first real programming summer job while attending the University of Waterloo. I learned ColdFusion and web development. I got to design and build an entire Content Management System. I even spoke at a couple conferences and was published in ColdFusion Developers Journal. I loved this world and knew that this was exactly where I wanted to be.

The first pivot

As much as I loved coding, I saw my bosses spend their time in meetings and writing emails, and I felt like they were superior to me somehow. In 2005, at twenty-four, I moved to Berlin, started this blog, and bought Consulting for Dummies. I wondered if maybe consulting work would be a step up the success ladder. I started freelancing, and everyone only wanted to pay me for coding. Since that's what I loved to do most, I didn't complain. I unpivoted, and thought I'd revisit the consulting idea another time.

The second pivot

My freelancing business grew, and I had to say "no" to a lot of people. Then I read "The E-Myth" and Peter Drucker, and decided my company needed to grow. At some point I had a dozen developers working for me, and even hired a Project Manager or two. I was able to say "yes" to everyone. I had pivoted to growing an agency, moving closer to this idea of success I kept reading about.

Then it stopped being fun. I would start off chatting with a new client, but turn it over to a PM when it started to get interesting. I was juggling so many projects in my mind, I barely had time or energy to do any coding. It felt like driving a car from the backseat.

One time, I had a developer stop answering emails, and had to jump in to save the project. I read through weeks of emails back and forth between the frustrated client, the project manager and the overwhelmed developer. The project became clear in my mind and I quickly built exactly what the client wanted in a weekend. I was back in the driver's seat, working directly with my client to build a web app.

I decided to unpivot. I stopped hiring. I sent my developers off to work with some of my clients directly. I wound it all down until it was just me building for a few good clients. I was back on the path again.

The third pivot

I'd always wanted to be a teacher. I was thrilled when my friend invited me to teach a programming course at Georgian College. I poured myself into teaching and loved everything about it. Even though I was teaching part time, all the prep work and marking really cut into my freelance work, and my income. At the end of the term, I asked my wife to remind me that I should probably say "no" next time.

Of course, I didn't say "no", because I was thrilled all over again the next time I was asked. And the next time. And the next time. I taught a half dozen classes over the next years, and absolutely loved helping students. I loved thinking about how to explain programming concepts to my students.

But I was spending so much time creating assignments and tests, and marking the students just to give them an arbitrary grade. I loved teaching, but I was never passionate about the college system. I decided it wasn't worth it anymore and unpivoted again.

The fourth pivot

The teaching itch never left me, so I decided to start teaching online. That way I could help students directly without all the tests and marking. I started making YouTube videos, streaming on Twitch, and even developed and launched a video course, The Joy of Svelte. I started a newsletter and imagined this as the start of a series of courses.

Making a course took a lot more time and energy than I expected. It was also missing something from teaching students directly in the classroom. I was procrastinating my course development and spending all my time coding for clients. I felt bad about this, like I was stuck in a rut. Eventually, I realized I wasn't stuck at all. I was doing the thing I loved the most, helping people directly.

I stopped feeling bad about it, and let go of pressuring myself to be a course creator. I had unpivoted back to focusing on the thing I love the most, working directly with clients to build web apps.

The fifth pivot

I was watching YouTube videos and listening to podcasts about business. They talked about "hands-on" work as being undesirable. These motivational speakers made me feel bad for wasting my limited time coding when I could be helping more people with my knowledge and experience. They convinced me the only way forward would be to pivot away from coding and to focus on being a coach & consultant.

I put on a blazer, had business cards made, and attended a tech conference. I told everyone I was like a "personal trainer for developers". I had this vision of using my experience and teaching skills to help development teams.

Every time I worked with a team to coach them on how to build or fix things, I ended up just building prototypes for them. I found it way easier, and frankly more fun, to build things instead of just telling people how to do it themselves.

I realized the best way to help people was the way I always had, by building things for them directly.

Learning by experimenting

I learned that I crave challenge and novelty. With each pivot, I followed my curiosity, or my insecurities, and tried out something new. Even when it didn't work, I learned something new about myself.

Each time, I'm glad I only experimented by dipping a toe in the water and never dove in completely. I'm grateful for those clients that stuck with me from the beginning, some of whom I'm still working with to this day.

Having gone through this cycle so many times, it's now comically clear that I've always done exactly what I should be doing. I'm a Web Developer, I build web apps for my clients, and I've been doing it non-stop since 2001.

I've stopped looking for greener pastures, and can now see how wonderful things are right now. I don't need anything different. I get to do even more of what I love, what I've always loved.

At least until the next pivot comes along.

Published on February 16th, 2026. © Jesse Skinner

Chrome broke my client's web app

Google Chrome icons Photo by Growtika on Unsplash

The great thing about the web is that web pages from thirty years ago still work fine. So you can imagine how surprised I was when a new version of Chrome broke my client's web app in production.

Business as usual

The web app is a data visualization tool with a colourful d3 SVG map. Hovering over a country brings up a tooltip, and clicking a country brings up a larger panel with data. Although the application was originally developed in 2013, I took it over in 2022.

I was working on some changes to the web app a couple of weeks ago. I run Linux (Arch btw) so I always have the latest version of everything, including Chrome. I spun up the development site one day and suddenly the map tooltips and clicking stopped working.

Did I install something new that broke the interactions? Maybe one of my Chrome extensions was messing with something? I tried rebooting, as one does when they're out of ideas. Still didn't work.

I loaded up Firefox. Everything worked fine. I grabbed my mac and tested it out in Chrome. Everything worked fine there too. I sent off my changes for the day for my client to review, and they didn't say anything about it being broken.

I could have left it at that, but it didn't sit right with me. That evening I started digging into it more.

Classically debugging a hard bug

First thing I did was add console.log statements to the d3 event handlers.

I could see that the mouseenter and mouseleave events were working, but the mousemove and click handlers never fired. I tried remove the mouseenter and mouseleave handlers, and suddenly the mousemove and click started working again.

I put back the mouseenter and mouseleave and looked for something that could have been responsible. Then I found this suspicious line:

// bring it to the front and maintain full stroke
path.parentNode.appendChild(path);

I removed it and it started working again! But that line was written six years ago, before I ever worked on the app. Why would that be to blame? What changed?

It Is Never a Compiler Bug

There's a trope in programming that "it's never a compiler bug". As a web developer, we would say "it's never a browser bug". Well could it be this time?

I was running Chrome v144 so I found the release notes. It came out on January 13th, so the timing made sense.

I scrolled and skimmed and finally found something that sounded like it might be relevant:

Interoperable pointer and mouse boundary events after DOM changes

After an event target is removed from the DOM, the logical target of the pointer, as implied by the Pointer and Mouse boundary events (that is, over, out, enter, and leave events), should be the nearest ancestor still attached to the DOM.

This led me to find a bug filed for Chromium v144, Regression in Chrome 144: Moving event target in "mouseenter" handler breaks click+mouseleave events. It was a Chrome bug! The example code even matched mine almost exactly:

redSquare.parentNode.appendChild(redSquare);

It seems like this is a permanent change due to a refinement of the spec. It's apparently a feature-not-a-bug, called BoundaryEventDispatchTracksNodeRemoval, so it's probably never going to be fixed.

There's fortunately a suggestion in a comment of how to fix this on my end though:

if (elem.parentNode.lastChild !== elem) {
    elem.parentNode.appendChild(elem);
}

Makes total sense! I made the change, and everything was working just as it had been a couple of weeks earlier!

Always trust your gut

I'm going to resist blaming the browser next time I have a bug, but it's very interesting to be reminded that it can and does happen.

The important takeaway is that when I see something wrong on a client project, I'm not going to be able to rest until I figure out exactly why. I needed to be sure it was limited to my computer, because if it wasn't, the last thing I'd want is for my client to find out from one of their users.

I of course pushed the change that night and let my client know what had happened, and that the production site was already being affected. Fortunately, v144 hadn't been fully rolled out. Even as I write this, less than 4% of users are running v144. This also underscores the importance of developers testing using the newest versions.

As a developer, I always want to fix bugs before my client notices, and definitely before their users notice.

Published on February 14th, 2026. © Jesse Skinner

Coding with LLMs can still be fun

Do you love reviewing AI-generated code? Do you get a tickle of pure joy to find and criticize the mistakes and problems in hallucinatory slop? Me neither.

You know what I do love? I love pouring my creativity and insight and empathy into a project. I love designing architectures and solutions that actually make things better for users. I love getting in the flow state, cranking away at a problem, building brick upon brick until the creation comes to life.

If you're not careful, AI tools will have you spending all your time doing code review. It's very hard to get into a flow state when you're waiting on your agents and reviewing what they do. Fortunately, I've found a way to use LLMs to do a lot more of what I love.

Curiosity and excitement

Since I was a child, I wondered how it was possible that humans were able to get a computer to do so many things. It felt like magic, and I wanted to be that magician. Forty years later, that feeling still guides me through my software development career.

While LLMs stir that curiosity and excitement, they also threaten to take it away. If, like me, you have very mixed feelings about it all, you might be both excited and worried about the potential.

I've found LLMs to be very helpful ever since Copilot came out in 2021, and they have definitely made me more productive. My views on coding with LLMs haven't changed much along the way. Five years later, I wanted to share with you how I'm using LLMs today, and how they're making my job more fun than ever.

A workflow that lets you work in flow

The secret is to find a workflow that works for you, that keeps you engaged and in a state of flow. I've found the following workflow that works really well for me. I use it every day for almost every multi-step coding task I work on. It basically starts with the following context:

When the user gives you a task specification:

1. Explore the codebase to find relevant files and patterns
2. Break the task into a small number of steps. Each step should include:
    a. a brief, high-level summary of the step
    b. a list of specific, relevant files
    c. quotes from the specification to be specific about what each step is for
3. Present the steps and get out of the way.

When the user says "done", "how's this", etc.:

1. Run git status and git diff to see what they changed
2. Review the changes and identify any potential problems
3. Compare changes against the steps and identify which steps are complete
4. Present a revised set of steps and get out of the user's way.

Important:
- Be concise and direct, don't give the user a lot to read
- Allow the user to make all technical, architectural and engineering decisions
- Present possible solutions but don't make any assumptions
- Don't write code - just guide
- Be specific about files and line numbers
- Trust them to figure it out

You can paste this in a new chat, or set up a "custom agent" or "skill" if you want to be fancy. I use a Claude Code skill for this, but it'll work with any LLM coding tools. Ideally it'll have access to your codebase and can work as a search engine to point you in the right place. I find this really speeds me up, especially on new codebases, or code I haven't touched in a long time.

With this workflow, I'm not spending hardly any time waiting on the LLM to generate content. I find this approach uses the LLM for what it does best. Hallucinations are almost non-existent, because everything it needs is available in the present context. There's very little sycophancy, and not much back-and-forth chatting at all.

I specifically don't want to be reading through pages of markdown. If it makes a mistake at a high-level, I can easily spot and correct it, or just choose to do something different.

Often I'll still decide to ask the LLM to write code for me, but I keep it limited to a small step in this process at a time. I sometimes have it scaffold out some empty modules for me while I work on a different step. Often the steps are very simple or mechanical, so it's actually easier to have the LLM complete it for me. I'll use it where LLM-generated code can speed me up, where I'm not wasting time babysitting or directing it to do better. I'm in full control of choosing which parts I want to work on, and which I don't. Either way, I'm staying fully engaged, and I know what's happening at any given moment. It's like wearing a jetpack that I'm steering, rather than a team of minions that I have to manage.

This workflow is easy to modify. You can change it to suit your preferences, and add rules that make it work better for you. You can do less of the coding if you want, or even use it as a planning step in your vibe coding.

It's also dead simple to understand, with nothing special you have to learn. You're not messing around with prompt engineering. There are no MCP servers to install, no special plugins. It works with all models, even cheap or local models. You don't have to keep up with the latest techbro videos to make a workflow that works for you.

Find your own workflow

I invite you to find a workflow that empowers you to do your best work while staying out of your way. Stay more engaged, lose track of time, reduce friction, solve problems and do your best work.

Please share what works for you, and if you have any suggestions to make coding with LLMs even more fun. I hope we can all learn from and inspire each other to make coding more fun than ever.

Published on January 17th, 2026. © Jesse Skinner

Weekly Cadence

I have a weekly cadence with my clients. This means they expect me to make progress every week. But there's no expectations for any particular day.

I'm also careful not to over-commit. I leave myself room to breathe. If I decide to go beyond that and work extra, everyone's happy. If I don't, that's okay too.

My day is a blank canvas. I get to decide how I'll fill it. What am I most curious or excited about? What do I want to work on, if anything?

Being able to set your own hours as a freelancer doesn't mean choosing 8-4 vs 10-6. It's being free to take a day off to play with your kids or finish an exciting book. It means being able to live the cozy life you need for a healthy body and mind.

Ultimately it means my work is higher quality because I'm not rushing, I'm well rested, and I love the work I do.

A weekly cadence won't work for all clients, nor for all freelancers, but it suits me and my clients perfectly.

Published on September 4th, 2025. © Jesse Skinner

Back to School & Back to Work

The past few months, for the first time, I prioritized coziness & rest, minimizing stress and ambition and mindfully enjoying travelling with my family.

I just dropped my son off at school, and now I feel it's time to start a new season of focusing on my business again.

But I feel the need to bring that coziness back into my professional life and find ways to do bigger things while still being restful and relaxed.

It'll be a big challenge, but it's very important to me to get this right.

I don't want to hustle. I'm not desperate to be more successful. What I desire is more creativity, more freedom, more fun, following my curiosity and joy and excitement through my work. I want to help more people, particularly freelancers like myself. I want to go beyond my client work and work on projects for myself.

In the past it's been hard to juggle that stuff with my need to trade hours for money. That's where the challenge comes in, to do more but to keep it fun and cozy and stress free.

I think I've come to see how lowering expectations for myself really helps to take the pressure off. I'm hoping to thread that needle of doing more in some areas, maybe doing less in others to compensate, and being very mindful and measured in making sure I'm able to handle it all gracefully.

Published on September 2nd, 2025. © Jesse Skinner
<< older posts

Contact Jesse

Need help with a project? Any questions about my writing? I'd love to hear from you.

Email me at [email protected]