<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kamil Buksakowski</title>
    <description>The latest articles on DEV Community by Kamil Buksakowski (@kamilbuksakowski).</description>
    <link>https://dev.to/kamilbuksakowski</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1140350%2Fc40a9412-52a1-426a-9f2b-6cd86a3b3954.jpeg</url>
      <title>DEV Community: Kamil Buksakowski</title>
      <link>https://dev.to/kamilbuksakowski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kamilbuksakowski"/>
    <language>en</language>
    <item>
      <title>MySQL Too many connections: How I Debugged It and Scaled the Connection Pool</title>
      <dc:creator>Kamil Buksakowski</dc:creator>
      <pubDate>Sun, 15 Mar 2026 13:02:43 +0000</pubDate>
      <link>https://dev.to/kamilbuksakowski/mysql-too-many-connections-how-i-debugged-it-and-scaled-the-connection-pool-2nnl</link>
      <guid>https://dev.to/kamilbuksakowski/mysql-too-many-connections-how-i-debugged-it-and-scaled-the-connection-pool-2nnl</guid>
      <description>&lt;p&gt;&lt;em&gt;Practical guide to the MySQL Too many connections error — from a local test to RDS Proxy.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lately, I’ve spent quite a lot of time debugging and understanding how database connections actually work. Then the topic of scaling those connections also came up.&lt;/p&gt;

&lt;p&gt;I wrote this article to share the conclusions and observations I arrived at. It also includes a &lt;strong&gt;simple local test&lt;/strong&gt; that helps explain how DB connections work and what the &lt;code&gt;Too many connections&lt;/code&gt; error really means.&lt;/p&gt;

&lt;p&gt;This is not an article written from the perspective of textbook theory. It’s more of a practical take after spending time analyzing the problem, running tests, and observing how the application behaves.&lt;/p&gt;

&lt;p&gt;If you spot a mistake here or think something could be explained better — feel free to let me know.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What MySQL Too many connections really means&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The message itself is pretty simple.&lt;/p&gt;

&lt;p&gt;In practice, this error means that the total demand for database connections is greater than the number of connections available on the DB side — in other words, greater than &lt;code&gt;max_connections&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In SQL, you set the &lt;code&gt;max_connections&lt;/code&gt; parameter according to your database resources and your infrastructure setup. Depending on what your application and environment look like, that number will vary.&lt;/p&gt;

&lt;p&gt;Let’s take a simple example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;2 GB RAM&lt;/li&gt;
&lt;li&gt;one database instance&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_connections&lt;/code&gt; = 200&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let’s assume the following setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the database is running on a server,&lt;/li&gt;
&lt;li&gt;we have a web application hitting that database,&lt;/li&gt;
&lt;li&gt;a developer connects to the same database locally from their machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point alone, we already have at least two sources generating DB connections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the web application,&lt;/li&gt;
&lt;li&gt;the developer’s local machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now add the fact that the developer also connects through a client like DBeaver to inspect the data. That’s another connection, or another use of the existing pool.&lt;/p&gt;

&lt;p&gt;Now add functions like &lt;code&gt;await Promise.all()&lt;/code&gt;, where we fire off multiple database queries in parallel.&lt;/p&gt;

&lt;p&gt;And this is exactly where we start moving toward the risk of eventually seeing: &lt;code&gt;Too many connections&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A simple max_connections = 10 example&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To illustrate it better, let’s take an example where &lt;code&gt;max_connections&lt;/code&gt; equals 10.&lt;/p&gt;

&lt;p&gt;Let’s assume the web application uses between 2 and 8 connections at peak.&lt;/p&gt;

&lt;p&gt;If it reaches 8, that leaves 2 free.&lt;/p&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one developer connects to the database through DBeaver,&lt;/li&gt;
&lt;li&gt;the total becomes 9,&lt;/li&gt;
&lt;li&gt;another developer does the same,&lt;/li&gt;
&lt;li&gt;the total becomes 10.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And at that point, we are already at the limit.&lt;/p&gt;

&lt;p&gt;Any additional client trying to connect may get &lt;code&gt;Too many connections&lt;/code&gt;, because it would be trying to become the eleventh connection to the database.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of message you will also see on the DB client side when you try to connect to a database that no longer has any free connections.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F45f1upat4xj7y3qso8s6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F45f1upat4xj7y3qso8s6.png" alt=" " width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is obviously a simplified model, but it explains the mechanics of the problem well.&lt;/p&gt;

&lt;p&gt;The same can be applied to production.&lt;/p&gt;

&lt;p&gt;If the total demand for connections exceeds &lt;code&gt;max_connections&lt;/code&gt;, sooner or later you will get an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where connection spikes and overages come from&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Most often, they come from a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;poorly written code can generate too much parallel traffic to the DB,&lt;/li&gt;
&lt;li&gt;connections are not being released properly,&lt;/li&gt;
&lt;li&gt;there is no sensible pool limit configured on the application side,&lt;/li&gt;
&lt;li&gt;the application scales, but the database configuration does not keep up with the number of instances,&lt;/li&gt;
&lt;li&gt;additional consumers of the same database appear: DBeaver, local tests, jobs, cron, migrations, integrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing is worth clarifying here.&lt;/p&gt;

&lt;p&gt;When I talk about &lt;code&gt;Promise.all()&lt;/code&gt;, I do not mean that &lt;code&gt;Promise.all()&lt;/code&gt; magically creates new connections by itself. I mean situations where you run many database queries in parallel inside &lt;code&gt;Promise.all()&lt;/code&gt;, which increases simultaneous demand for DB resources.&lt;/p&gt;

&lt;p&gt;That is an important difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why setting an application-side connection pool limit matters&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In my opinion, this should be standard.&lt;/p&gt;

&lt;p&gt;If your DB has &lt;code&gt;max_connections&lt;/code&gt; = 10, and in your backend code you set a limit so the application can use at most 5 connections, then a single backend instance should not be able to consume the entire pool by itself.&lt;/p&gt;

&lt;p&gt;That does not mean the global problem disappears. Other connection sources still exist. But it does mean the backend stops behaving aggressively toward the database.&lt;/p&gt;

&lt;p&gt;And this leads to an interesting side effect.&lt;/p&gt;

&lt;p&gt;If the application cannot use more than 5 connections and more traffic comes in, some requests will simply wait longer. So instead of an error, you get increased response time.&lt;/p&gt;

&lt;p&gt;Of course, this is not a guarantee that the error will never appear, because the same database may also be used by other processes, other application instances, or additional clients.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;By setting a limit on the application side, you reduce the risk of &lt;code&gt;Too many connections&lt;/code&gt;, but during peak hours, once the available connection pool is exhausted, some things will simply run slower.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;My local MySQL Too many connections test&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To understand this better, I ran a simple test.&lt;/p&gt;

&lt;p&gt;The setup looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a local database running in Docker,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_connections&lt;/code&gt; = 15,&lt;/li&gt;
&lt;li&gt;an endpoint that executed more than 15 parallel DB queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To start with, I set a low local &lt;code&gt;max_connections&lt;/code&gt; value so I could trigger the problem easily and observe it in controlled conditions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F00gbauem0n33xkl9z62x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F00gbauem0n33xkl9z62x.png" alt=" " width="504" height="98"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Variant 1 - without an application-side limit&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I called the endpoint.&lt;/p&gt;

&lt;p&gt;Result: the &lt;code&gt;Too many connections&lt;/code&gt; error appeared.&lt;/p&gt;

&lt;p&gt;Here you can see the effect on the NestJS application side — once the connection limit was exceeded, the backend started returning &lt;code&gt;Too many connections&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbr9ijic8gmof900s4hto.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbr9ijic8gmof900s4hto.png" alt=" " width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Variant 2 — with an application-side limit&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I added a parameter so the backend could not use more than 10 connections.&lt;/p&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;p&gt;the error disappeared, but the request took longer to complete.&lt;/p&gt;

&lt;p&gt;And this is one of the most important conclusions from the whole article for me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A code-side limit is a simple protective mechanism that helps reduce the risk of exhausting DB connections.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to verify this yourself, I encourage you to test it on the &lt;a href="https://github.com/KamilBuksa/mysql-too-many-connections-repro" rel="noopener noreferrer"&gt;mysql-too-many-connections-repro&lt;/a&gt; repository. (&lt;a href="https://github.com/KamilBuksa/mysql-too-many-connections-repro" rel="noopener noreferrer"&gt;https://github.com/KamilBuksa/mysql-too-many-connections-repro&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What is the right number of connections?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In my opinion, that is the wrong question.&lt;/p&gt;

&lt;p&gt;A better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;what number of connections is appropriate for my infrastructure?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And this is where things start getting interesting.&lt;/p&gt;

&lt;p&gt;Because the answer depends on things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether your backend scales vertically or horizontally,&lt;/li&gt;
&lt;li&gt;how many application instances you have,&lt;/li&gt;
&lt;li&gt;how many different clients use the same database,&lt;/li&gt;
&lt;li&gt;how intensively you use parallel queries,&lt;/li&gt;
&lt;li&gt;what the traffic looks like.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you scale the backend vertically, the topic is simpler. You have one instance, more resources, and you can tune the pool and &lt;code&gt;max_connections&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you scale the backend horizontally, it gets harder.&lt;/p&gt;

&lt;p&gt;Because then you need not only to increase &lt;code&gt;max_connections&lt;/code&gt;, but also to calculate how much &lt;strong&gt;one instance&lt;/strong&gt; of the application should be allowed to consume at most.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;max_connections&lt;/code&gt; = 15&lt;/li&gt;
&lt;li&gt;3 backend instances&lt;/li&gt;
&lt;li&gt;you configure 5 connections per instance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first glance, it looks fine.&lt;/p&gt;

&lt;p&gt;But once you add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DBeaver,&lt;/li&gt;
&lt;li&gt;a developer’s local connection,&lt;/li&gt;
&lt;li&gt;another process,&lt;/li&gt;
&lt;li&gt;a job runner,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;you quickly realize that 3 x 5 is no longer such an ideal setup.&lt;/p&gt;

&lt;p&gt;And that is exactly why this topic is context-dependent. There is no single answer for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Scaling the backend is not the same as scaling MySQL&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;These are two separate things.&lt;/p&gt;

&lt;p&gt;You can scale the backend by adding more instances, while the database itself remains the same. And that is exactly when connection management stops being “a local problem of one application.”&lt;/p&gt;

&lt;p&gt;If the backend scales horizontally and the application starts gaining traffic, sooner or later you end up asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;how do I manage connections so that several instances do not kill one database?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that is where proxy comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How RDS Proxy helps with MySQL connection management&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I’ll show this with AWS RDS Proxy as an example.&lt;/p&gt;

&lt;p&gt;My simplified mental model looked like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;without a proxy, if the total number of connections generated by the application and all instances together exceeds &lt;code&gt;max_connections&lt;/code&gt;, errors will start appearing,&lt;/p&gt;

&lt;p&gt;with a proxy, part of that traffic will be handled by a layer managing the connection pool, and instead of an immediate error, some requests will simply wait longer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;without a proxy, you will see &lt;code&gt;Too many connections&lt;/code&gt; sooner,&lt;/li&gt;
&lt;li&gt;with a proxy, the system has a better chance of spreading the problem over time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was exactly what interested me the most.&lt;/p&gt;

&lt;p&gt;Put simply: the application no longer connects directly to the database, but to an intermediate layer that manages the DB connection pool. Because of that, a large number of clients on the application side does not have to mean exactly the same number of physical connections to the database itself.&lt;/p&gt;

&lt;p&gt;It is also worth clarifying that a proxy does not magically make the database able to handle everything. If the real problem is heavy queries or long-running transactions, the proxy will not fix that. Its job is mainly to manage connections better.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;My working hypothesis about RDS Proxy and Too many connections&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;My hypothesis was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;if, without RDS Proxy, I trigger a scenario where the sum of connections used by the application exceeds &lt;code&gt;max_connections&lt;/code&gt; on the database side, I will get a &lt;code&gt;Too many connections&lt;/code&gt; error.&lt;/p&gt;

&lt;p&gt;If I connect the application through RDS Proxy, then in that same scenario the request will not fail immediately — it will wait for a free connection and complete later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was exactly the mechanism I wanted to confirm with a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Test 1: MySQL Too many connections without RDS Proxy&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;On an EC2 server, I deployed an endpoint that, when called, exceeded &lt;code&gt;max_connections&lt;/code&gt; in the DB.&lt;/p&gt;

&lt;p&gt;I also disabled the application-side limit so that the application would not block me from exceeding the limit and I could trigger the problem directly.&lt;/p&gt;

&lt;p&gt;After calling the endpoint:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Too many connections&lt;/code&gt; errors appeared.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Test 2: MySQL connections behavior with RDS Proxy&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The setup was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same endpoint exceeding the number of connections,&lt;/li&gt;
&lt;li&gt;no application-side limit,&lt;/li&gt;
&lt;li&gt;in &lt;code&gt;.env&lt;/code&gt;, a connection through RDS Proxy instead of directly to RDS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After calling the endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the error did not appear,&lt;/li&gt;
&lt;li&gt;the endpoint took longer to execute,&lt;/li&gt;
&lt;li&gt;but in the end the request completed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that was exactly the effect I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Is setting a connection pool limit enough?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Not always.&lt;/p&gt;

&lt;p&gt;In my opinion, a code-side limit is mainly enough when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you have one backend instance,&lt;/li&gt;
&lt;li&gt;you scale mostly vertically,&lt;/li&gt;
&lt;li&gt;your traffic is predictable,&lt;/li&gt;
&lt;li&gt;and the whole setup is fairly simple.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In that case, a proxy may be unnecessary.&lt;/p&gt;

&lt;p&gt;But if you have several backend instances, it becomes a different story.&lt;/p&gt;

&lt;p&gt;Because the code-side limit works locally for one instance, while RDS Proxy works at a higher level — as a connection-management layer for a larger number of clients.&lt;/p&gt;

&lt;p&gt;In simplified terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the code-side limit controls one instance,&lt;/li&gt;
&lt;li&gt;the proxy helps control the problem more broadly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the user’s perspective, it is obviously better if a request waits a little longer instead of failing immediately. In many cases that is exactly what will happen, although under heavier overload you can still get timeouts or other errors.&lt;/p&gt;

&lt;p&gt;And that is exactly the practical value of the proxy for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What I concluded from the MySQL connection pool tests&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Based on these tests, I confirmed one practical observation for myself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;when the total number of DB connections starts exceeding what the database can safely handle directly, RDS Proxy can turn part of those immediate failures into waiting for a resource.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And yes — someone could say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;but you did not actually test this with several additional backend instances running at once&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is true. I did not run a full 1:1 autoscaling simulation with several instances.&lt;/p&gt;

&lt;p&gt;But the behavior itself was clear to me:&lt;/p&gt;

&lt;p&gt;If, without the proxy, exceeding the limit causes errors, and with the proxy, in a similar scenario, the request simply starts waiting longer, then you can see what class of problem this solution addresses.&lt;/p&gt;

&lt;p&gt;That was enough of a signal for me to better understand the purpose of the proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What is the best connection pool size?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;There is no single answer.&lt;/p&gt;

&lt;p&gt;Context is king.&lt;/p&gt;

&lt;p&gt;It depends on the system, traffic, number of instances, the intensity of parallel queries, and how many other processes use the same database.&lt;/p&gt;

&lt;p&gt;You cannot do this properly once and for all in isolation from the context.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;When RDS Proxy actually makes sense&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In my opinion, not blindly.&lt;/p&gt;

&lt;p&gt;At the very beginning of a project, RDS Proxy will very often be overkill and an unnecessary extra cost.&lt;/p&gt;

&lt;p&gt;The hard truth is that most projects never even reach the point where scaling DB connections becomes a real problem.&lt;/p&gt;

&lt;p&gt;So first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;observe the traffic,&lt;/li&gt;
&lt;li&gt;observe how the application evolves,&lt;/li&gt;
&lt;li&gt;watch whether you are actually reaching the stage where instances are being added and connections start competing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In many cases, instead of implementing a proxy, companies simply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;increase DB RAM,&lt;/li&gt;
&lt;li&gt;increase DB resources,&lt;/li&gt;
&lt;li&gt;increase &lt;code&gt;max_connections&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And honestly? That is okay too.&lt;/p&gt;

&lt;p&gt;It is faster, simpler, and often enough.&lt;/p&gt;

&lt;p&gt;Only later, when costs start rising or the setup becomes harder to manage, does real interest in a proxy begin.&lt;/p&gt;

&lt;p&gt;From what I have seen, companies are more likely to first increase DB resources and &lt;code&gt;max_connections&lt;/code&gt; than to jump straight into RDS Proxy. And honestly, that does not surprise me at all.&lt;/p&gt;

&lt;p&gt;RDS Proxy is more advanced. It gives you more possibilities, but it also requires more work, more understanding, and changes how the application connects to the database — which by itself can create new complexity in an existing setup.&lt;/p&gt;

&lt;p&gt;So I would not treat it as the default first move.&lt;/p&gt;

&lt;p&gt;But it is definitely worth understanding how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Final thoughts on MySQL Too many connections&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Too many connections&lt;/code&gt; is rarely the problem of one function or one endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is a sum problem&lt;/strong&gt; — all the clients, processes, and instances that use the same database at the same time.&lt;/p&gt;

&lt;p&gt;An application-side limit is the first and simplest protective mechanism. It is worth setting. But with many backend instances, that alone often stops being enough — and that is exactly where a proxy starts doing useful work.&lt;/p&gt;

&lt;p&gt;That does not mean you need to implement it right away. Often, it is enough to first understand your setup and observe how the system behaves under load.&lt;/p&gt;

&lt;p&gt;If this article helps someone understand the topic faster than it took me the first time around, that is great.&lt;/p&gt;

&lt;p&gt;Thanks for your time.&lt;/p&gt;

</description>
      <category>mysql</category>
      <category>database</category>
      <category>aws</category>
      <category>backend</category>
    </item>
    <item>
      <title>Letting Claude Code Test Your Backend - Verifying Business Logic via API and SQL</title>
      <dc:creator>Kamil Buksakowski</dc:creator>
      <pubDate>Sun, 08 Mar 2026 12:07:16 +0000</pubDate>
      <link>https://dev.to/kamilbuksakowski/letting-claude-code-test-your-backend-verifying-business-logic-via-api-and-sql-1635</link>
      <guid>https://dev.to/kamilbuksakowski/letting-claude-code-test-your-backend-verifying-business-logic-via-api-and-sql-1635</guid>
      <description>&lt;p&gt;What if an &lt;strong&gt;AI agent could test your backend by calling API endpoints and verifying results directly in SQL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I tried exactly that using Claude Code. The agent &lt;strong&gt;called API endpoints, inspected a Docker database via SQL, and validated a decision tree of business logic scenarios automatically.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a single operation triggers &lt;strong&gt;cascading changes across multiple entities,&lt;/strong&gt; the number of scenarios grows quickly. In practice, this often ends up with manually clicking endpoints in Postman and checking the database state after every operation.&lt;/p&gt;

&lt;p&gt;Instead of running tests manually, I wrote down the decision tree of scenarios in a markdown file and handed it to Claude Code. Then I gave it access to a local Docker database and an API service that seeds test data.&lt;/p&gt;

&lt;p&gt;Claude &lt;strong&gt;executed operations through the API, verified the system state using SQL queries,&lt;/strong&gt; analyzed the results, and reported PASS / FAIL for each scenario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In practice, it behaves like an agent running integration tests — but without writing test code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this article, I will show how to &lt;strong&gt;let Claude Code call your API, inspect a local database, and automatically validate complex decision trees.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This workflow is still experimental, but it demonstrates an interesting direction for backend testing automation — especially in systems with complex business logic and many edge-case combinations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools used in this article:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code&lt;/li&gt;
&lt;li&gt;Docker Desktop 29.1.5 (Mac)&lt;/li&gt;
&lt;li&gt;DBeaver — database client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Demo repository: &lt;a href="https://github.com/KamilBuksa/claude-code-local-db-testing" rel="noopener noreferrer"&gt;github.com/KamilBuksa/claude-code-local-db-testing&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Running a Local Database with Docker
&lt;/h2&gt;

&lt;p&gt;We start by setting up the environment. The repository includes a ready-to-use &lt;code&gt;docker-compose.yml&lt;/code&gt; with MariaDB — one command is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the containers are running, we can connect to the database. In Docker Desktop it should look like this:&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzx139q41d9g5yhcu8n7c.png" alt=" " width="800" height="294"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Viewing the Database in DBeaver
&lt;/h2&gt;

&lt;p&gt;To see what's happening in the database in real time, I connected DBeaver.&lt;/p&gt;

&lt;p&gt;Open DBeaver → New Database Connection → choose MariaDB:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2swq2jteaym4jf4r0m1x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2swq2jteaym4jf4r0m1x.png" alt=" " width="800" height="663"&gt;&lt;/a&gt;&lt;br&gt;
Click &lt;strong&gt;Test Connection&lt;/strong&gt; — it should display "Connected (59ms)". Then click &lt;strong&gt;Finish&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfzzjaai2nqz59we6u4d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfzzjaai2nqz59we6u4d.png" alt=" " width="800" height="672"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After connecting, the full database structure becomes visible (to generate the structure, run &lt;code&gt;npm run start:dev&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpt1jjq4fe09ofo5im2eo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpt1jjq4fe09ofo5im2eo.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything is ready.&lt;/p&gt;


&lt;h2&gt;
  
  
  Domain: HR with Cascading Statuses
&lt;/h2&gt;

&lt;p&gt;The application models a company operating across multiple offices. Employees belong to departments, and departments operate inside buildings. Each entity has its own lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ACTIVE&lt;/code&gt; — has at least one active department with employees&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VACANT&lt;/code&gt; — all departments are empty or disbanded&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CLOSED&lt;/code&gt; — manually closed, does not change automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Department&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ACTIVE&lt;/code&gt; — has at least one employee&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EMPTY&lt;/code&gt; — the last employee left the department (the department still exists)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DISBANDED&lt;/code&gt; — dissolved by an admin or automatically when the last employee becomes deactivated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Employee&lt;/strong&gt; — &lt;code&gt;ACTIVE&lt;/code&gt; or &lt;code&gt;DEACTIVATED&lt;/code&gt;. Each employee can belong to multiple departments with roles &lt;code&gt;MANAGER&lt;/code&gt; or &lt;code&gt;MEMBER&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The key cascading rule: when a department changes status, the system checks whether the building should change to &lt;code&gt;VACANT&lt;/code&gt;. Conversely, when an active department appears, the building returns to &lt;code&gt;ACTIVE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I defined five edge-case scenarios for three operations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Remove employee from department&lt;/td&gt;
&lt;td&gt;The only active department in the building. The only employee voluntarily leaves&lt;/td&gt;
&lt;td&gt;Department: &lt;code&gt;EMPTY&lt;/code&gt; · Building: &lt;code&gt;VACANT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Disband department&lt;/td&gt;
&lt;td&gt;The only department in the building&lt;/td&gt;
&lt;td&gt;Department: &lt;code&gt;DISBANDED&lt;/code&gt; · Building: &lt;code&gt;VACANT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Disband department&lt;/td&gt;
&lt;td&gt;Other departments in the building remain active&lt;/td&gt;
&lt;td&gt;Department: &lt;code&gt;DISBANDED&lt;/code&gt; · Building: &lt;code&gt;ACTIVE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Deactivate employee&lt;/td&gt;
&lt;td&gt;The only active department in the building. The only employee is deactivated&lt;/td&gt;
&lt;td&gt;Department: &lt;code&gt;DISBANDED&lt;/code&gt; · Building: &lt;code&gt;VACANT&lt;/code&gt; · Employee: &lt;code&gt;DEACTIVATED&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Deactivate employee&lt;/td&gt;
&lt;td&gt;The employee is not the only person in any department&lt;/td&gt;
&lt;td&gt;Departments and buildings: &lt;code&gt;ACTIVE&lt;/code&gt; · Employee: &lt;code&gt;DEACTIVATED&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What happens step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The only employee voluntarily leaves the department → the department becomes &lt;code&gt;EMPTY&lt;/code&gt;. The building no longer has any active departments, so it becomes &lt;code&gt;VACANT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When an admin disbands a department, it becomes &lt;code&gt;DISBANDED&lt;/code&gt;. Because it was the only department in the building, the building becomes &lt;code&gt;VACANT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Disbanding one department does not affect the building if other departments are still active — the building remains &lt;code&gt;ACTIVE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Deactivating the employee automatically disbands the department (&lt;code&gt;DISBANDED&lt;/code&gt;) instead of merely emptying it (&lt;code&gt;EMPTY&lt;/code&gt;) — this is the key difference compared to case 1.&lt;/li&gt;
&lt;li&gt;Deactivating an employee who is not the only member of any department does not trigger any cascade — departments and buildings remain unchanged.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Prompt for Claude Code
&lt;/h2&gt;

&lt;p&gt;The decision tree — the full table of scenarios with setups and expected results — is stored in &lt;code&gt;docs/test-cases.md&lt;/code&gt; in the repository. Claude has access to it via &lt;code&gt;CLAUDE.md&lt;/code&gt;. I used a single short prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seed the database, then test each scenario from @docs/test-cases.md.
For every case: set up the state, call the endpoint, verify via SQL, report PASS / FAIL.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude read the file with the test cases, planned the execution order, and started working.&lt;/p&gt;




&lt;h2&gt;
  
  
  Claude Code in Action
&lt;/h2&gt;

&lt;p&gt;Claude started by verifying that the API was running:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fge0wvr2xpyktwpppwcsm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fge0wvr2xpyktwpppwcsm.png" alt=" " width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, a curl request to &lt;code&gt;/buildings&lt;/code&gt; to confirm the service responds. Then it loaded the test data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2f55lq048tetf9jpji47.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2f55lq048tetf9jpji47.png" alt=" " width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/mock/seed&lt;/code&gt; endpoint creates a complete dataset: buildings, departments, employees, and their memberships. Claude saved the returned IDs and proceeded with the tests.&lt;/p&gt;

&lt;p&gt;To verify the database state, Claude used &lt;code&gt;docker exec&lt;/code&gt; &lt;strong&gt;to run SQL queries directly:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg47alyz0zwu9omgrc2a3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg47alyz0zwu9omgrc2a3.png" alt=" " width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude did not just execute queries — it analyzed the results and understood the context, explaining why the result was correct before moving to the next case.&lt;/p&gt;




&lt;h2&gt;
  
  
  First Test: Case 4
&lt;/h2&gt;

&lt;p&gt;Claude began with Case 4 — deactivating the only employee in the department:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvh2z4d5jok1odfn9nvb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvh2z4d5jok1odfn9nvb.png" alt=" " width="800" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;State reset, minimal setup: one building, one IT department, and one employee (John) as the only manager. After deactivating John, the expected result is: department → &lt;code&gt;DISBANDED&lt;/code&gt;, building → &lt;code&gt;VACANT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxit45mt9pbmqmyzylu9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxit45mt9pbmqmyzylu9.png" alt=" " width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 4: PASS.&lt;/strong&gt; The cascading logic works correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parallel Tests
&lt;/h2&gt;

&lt;p&gt;After verifying Case 4, I asked Claude to run the remaining scenarios in parallel. It prepared the following cases:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcd72p2yh5ob2v6nuvqm2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcd72p2yh5ob2v6nuvqm2.png" alt=" " width="800" height="687"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each test received a separate agent, and all started simultaneously:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfd0s90okodaokdfnny7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfd0s90okodaokdfnny7.png" alt=" " width="518" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo9l1ggjihiqukza8g5fa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo9l1ggjihiqukza8g5fa.png" alt=" " width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All five scenarios: PASS.&lt;/strong&gt; The cascading business logic works correctly for every edge case.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo Repository
&lt;/h2&gt;

&lt;p&gt;The full demo is publicly available: &lt;a href="https://github.com/KamilBuksa/claude-code-local-db-testing" rel="noopener noreferrer"&gt;github.com/KamilBuksa/claude-code-local-db-testing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Simply follow the setup instructions in the &lt;code&gt;README.md&lt;/code&gt;, run &lt;code&gt;claude&lt;/code&gt; in the repository directory, and type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Let's play around and test the decision tree.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude will read the context from &lt;code&gt;CLAUDE.md&lt;/code&gt;, recall the decision tree, and guide the entire testing process:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxseqqxgcz26c9rtbsmp1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxseqqxgcz26c9rtbsmp1.png" alt=" " width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the screenshot you can see that Claude immediately starts working — it reads the context from &lt;code&gt;CLAUDE.md&lt;/code&gt; and begins by seeding the test data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Important Notes
&lt;/h2&gt;

&lt;p&gt;A few things to keep in mind before trying this yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never give Claude Code access to a production database&lt;/strong&gt; — it's unsafe. In this article we work with an isolated local environment running in Docker.&lt;/li&gt;
&lt;li&gt;The repository uses schema synchronization instead of migrations for quick setup — in production environments you should use migrations.&lt;/li&gt;
&lt;li&gt;All endpoints are public for testing purposes — this is not recommended for real applications.&lt;/li&gt;
&lt;li&gt;Tested on Docker Desktop 29.1.5 on Mac — if the &lt;code&gt;docker&lt;/code&gt; command does not work, you likely need a newer version.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;What happened here in short? Claude executed operations through curl calls to the API, verified SQL results via &lt;code&gt;docker exec&lt;/code&gt;, understood the context of the results, and reported PASS/FAIL.&lt;/p&gt;

&lt;p&gt;This workflow is still experimental. I had a lot of fun exploring Claude's behavior during this process. In real projects it may be useful to create a database dump from the testing environment and restore it locally to start with realistic data. Preparing the decision tree in a markdown file beforehand is also important.&lt;/p&gt;

&lt;p&gt;Personally, I never trust AI 100%, so I would still manually verify critical cases through the UI after connecting the frontend. However, this kind of testing can detect issues earlier and save time.&lt;/p&gt;

&lt;p&gt;This approach works particularly well when testing complex decision trees, where the number of scenarios grows quickly and manual testing becomes impractical.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Happy coding! 🚀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>backend</category>
      <category>docker</category>
    </item>
    <item>
      <title>Parallel work with Claude Code in iTerm2 - a workflow inspired by Boris Cherny</title>
      <dc:creator>Kamil Buksakowski</dc:creator>
      <pubDate>Mon, 16 Feb 2026 07:40:47 +0000</pubDate>
      <link>https://dev.to/kamilbuksakowski/parallel-work-with-claude-code-in-iterm2-a-workflow-inspired-by-boris-cherny-5940</link>
      <guid>https://dev.to/kamilbuksakowski/parallel-work-with-claude-code-in-iterm2-a-workflow-inspired-by-boris-cherny-5940</guid>
      <description>&lt;p&gt;A while ago, Boris Cherny's setup for working with Claude Code went viral. I noticed how he works in parallel across several terminals and how much it simplifies context management.&lt;/p&gt;

&lt;p&gt;In this article, I'm showing a workflow inspired by Boris's setup, extended with Git worktrees for real work without conflicts. I'm still testing this workflow in practice, but it already works well enough to be a valuable starting point for others. That's why this tutorial exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; iterm2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open iTerm2 via Spotlight (⌘ + Space, type "iterm").&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9a7102ychu4nqo82nyg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9a7102ychu4nqo82nyg.png" alt=" " width="800" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After launching, you'll see:&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa0t299srcjyfsrydhkl.png" alt=" " width="800" height="641"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Creating tabs
&lt;/h2&gt;

&lt;p&gt;Create several tabs with the shortcut:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⌘T
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat 5 times (or as many as you need).&lt;/p&gt;

&lt;p&gt;Switching between tabs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;⌘1&lt;/code&gt; – first tab&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;⌘2&lt;/code&gt; – second tab&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;⌘3&lt;/code&gt; – third tab&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default, all tabs have the name &lt;code&gt;-zsh&lt;/code&gt;, which quickly becomes hard to read.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz6shvzoi1hltcp6msfip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz6shvzoi1hltcp6msfip.png" alt=" " width="800" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Boris's setup looks much cleaner – each tab has a simple, numbered name.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhzmikcd2wo3l3ihwh9nl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhzmikcd2wo3l3ihwh9nl.png" alt=" " width="800" height="46"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Numbering tabs
&lt;/h2&gt;

&lt;p&gt;Double-click the name of the first tab (where it says &lt;code&gt;-zsh&lt;/code&gt;).&lt;br&gt;
A &lt;strong&gt;Set Tab Title&lt;/strong&gt; window will appear.&lt;/p&gt;

&lt;p&gt;Type the tab number (e.g., &lt;code&gt;1&lt;/code&gt; for the first, &lt;code&gt;2&lt;/code&gt; for the second) and click &lt;strong&gt;OK&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9snofcdaf539x01403oi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9snofcdaf539x01403oi.png" alt=" " width="800" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the screen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;red circle 1&lt;/strong&gt; – double-click the tab name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;red circle 2&lt;/strong&gt; – type the number and confirm&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repeat the operation for the remaining tabs (2, 3, 4, 5, …).&lt;/p&gt;

&lt;p&gt;Final result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv64feu1i178d0o5ig10u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv64feu1i178d0o5ig10u.png" alt=" " width="800" height="58"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Simple flow for small tasks
&lt;/h2&gt;

&lt;p&gt;Simplest approach: one tab = one branch.&lt;/p&gt;

&lt;p&gt;Before running Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature/auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach works well for small, independent tasks in different parts of the project.&lt;/p&gt;

&lt;p&gt;Keep in mind that when running the same project in several terminals, Claude Code works on the same file state. If tasks start touching similar code areas, conflicts can appear. That's why we use this approach consciously and only for simple cases.&lt;/p&gt;

&lt;p&gt;Here's how it looks for me:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbl9pnodsbrlu755mctuq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbl9pnodsbrlu755mctuq.png" alt=" " width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Extended flow for larger tasks
&lt;/h2&gt;

&lt;p&gt;When working on larger features or several tasks in parallel, just switching branches in one directory quickly becomes inconvenient.&lt;/p&gt;

&lt;h4&gt;
  
  
  Problem
&lt;/h4&gt;

&lt;p&gt;Claude Code operates on the filesystem state of a given folder, regardless of the current branch. Even if we have different branches open in several terminals, we're still working on the same working directory, which increases the risk of conflicts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Solution
&lt;/h4&gt;

&lt;p&gt;Git worktrees – multiple working directories, no conflicts.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setup Git Worktrees:
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1. Creating worktrees (e.g., at the start of the day)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/projects/my-super-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add ../worktrees/task1 &lt;span class="nt"&gt;-b&lt;/span&gt; feature/carrier-popup
git worktree add ../worktrees/task2 &lt;span class="nt"&gt;-b&lt;/span&gt; feature/email-validation
git worktree add ../worktrees/task3 &lt;span class="nt"&gt;-b&lt;/span&gt; feature/carrier-filters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Running Claude Code in each worktree&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Tab 1&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/projects/worktrees/task1
claude &lt;span class="s2"&gt;"Task 1 description"&lt;/span&gt;

&lt;span class="c"&gt;# Tab 2&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/projects/worktrees/task2
claude &lt;span class="s2"&gt;"Task 2 description"&lt;/span&gt;

&lt;span class="c"&gt;# Tab 3&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/projects/worktrees/task3
claude &lt;span class="s2"&gt;"Task 3 description"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each terminal now works on a separate folder and a separate branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Finishing feature and cleanup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After finishing work and code review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In each worktree&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: change description"&lt;/span&gt;
git push origin feature/branch-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Removing worktrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In main repo&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/projects/my-super-project
git worktree remove ../worktrees/task1
git worktree remove ../worktrees/task2
git worktree remove ../worktrees/task3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Notes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Worktrees outside repo (&lt;code&gt;../worktrees/&lt;/code&gt;) → do not require .gitignore&lt;/li&gt;
&lt;li&gt;Each worktree = separate branch = separate changes&lt;/li&gt;
&lt;li&gt;Each worktree has its own &lt;code&gt;node_modules&lt;/code&gt;, but shares &lt;code&gt;.git&lt;/code&gt; – operations on the repo are shared&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Notifications in iTerm2
&lt;/h2&gt;

&lt;p&gt;After Claude Code finishes a command, you can get a notification (sometimes with a slight delay).&lt;/p&gt;

&lt;p&gt;Open iTerm2 settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⌘ ,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Profiles&lt;/strong&gt; (top bar)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminal&lt;/strong&gt; (right panel)&lt;/li&gt;
&lt;li&gt;Check:

&lt;ul&gt;
&lt;li&gt;✓ Flash visual bell&lt;/li&gt;
&lt;li&gt;✓ Show bell icon in tabs&lt;/li&gt;
&lt;li&gt;✓ Notification Center Alerts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh01dszeflyaqktos92b1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh01dszeflyaqktos92b1.png" alt=" " width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, in &lt;strong&gt;System Settings&lt;/strong&gt; (macOS):&lt;br&gt;
Settings → Notifications → iTerm2 → Allow&lt;/p&gt;

&lt;p&gt;Result:&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiw50n3c2js14hf3e13lf.png" alt=" " width="724" height="240"&gt;
&lt;/h2&gt;
&lt;h2&gt;
  
  
  BONUS – keyboard shortcut conflict
&lt;/h2&gt;

&lt;p&gt;If you use &lt;code&gt;Command + Right Arrow&lt;/code&gt; in Claude Code to jump to the end of a line, iTerm2 might switch tabs.&lt;/p&gt;

&lt;p&gt;To disable this:&lt;/p&gt;

&lt;p&gt;1) Open settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⌘ ,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Go to &lt;strong&gt;Keys&lt;/strong&gt;&lt;br&gt;
3) Find: &lt;strong&gt;Previous Tab&lt;/strong&gt; and &lt;strong&gt;Next Tab&lt;/strong&gt;&lt;br&gt;
4) Set both to &lt;strong&gt;"-"&lt;/strong&gt; (minus = disabled)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ul2v0jmce7ycryc9krg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ul2v0jmce7ycryc9krg.png" alt=" " width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now &lt;code&gt;Command + Right Arrow&lt;/code&gt; works correctly in Claude Code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Numbered tabs, conscious use of branches, and Git worktrees let you work sensibly in parallel with Claude Code without fighting with context and code conflicts.&lt;/p&gt;

&lt;p&gt;This is a workflow that works especially well when working on multiple tasks at the same time.&lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

</description>
      <category>terminal</category>
      <category>claudecode</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Spec Driven Development with AI - How I Built My Portfolio Site</title>
      <dc:creator>Kamil Buksakowski</dc:creator>
      <pubDate>Wed, 14 Jan 2026 07:01:46 +0000</pubDate>
      <link>https://dev.to/kamilbuksakowski/spec-driven-development-with-ai-how-i-built-my-portfolio-site-1o5g</link>
      <guid>https://dev.to/kamilbuksakowski/spec-driven-development-with-ai-how-i-built-my-portfolio-site-1o5g</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Intro&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I want to share the process of building a website using AI. With smart AI usage, you really don't need programming skills to set up a portfolio site. This is a simple project you can do yourself. In this article, I'll show you the entire process. There are many AI tools out there, I'm a fan of Claude Code, so I went with that. The article goes from general to specific - the further down you read, the more nuances you'll find that are worth remembering and might slip past if you're new to this.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;AI as a smart advisor who always has time&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You can start your journey with the web version of Claude / ChatGPT or whatever. Treat AI as your entry point to the topic. The more you progress, the more tools you'll add - your personal advisor will help choose the right ones for your case. For me, it was Claude Code because I use it daily. I chose Cursor as my IDE, switched to it from free VS Code, which is also great but not as AI-friendly as Cursor. In short, Claude Code is the executor and Cursor is the environment where the executor works.&lt;/p&gt;

&lt;p&gt;Pro tip: If you're non-technical and some terms are foreign to you - use this simple trick: copy the article to AI and ask "What did the author mean by Cursor as IDE, are there free alternatives?". This way you can set up any setup you dream of, often changing tools. It takes some time but it's fun, I recommend it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What do I want to do? Brainstorming the site&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is the first phase where I think about what I want to achieve. No pressure, I casually write in a notepad what features I'd like to have, any notes, if I have a concept then what style to use, etc.&lt;/p&gt;

&lt;p&gt;For me it was something like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm planning to make my portfolio site. I'm a developer and I'd like it to reflect who I am, what my achievements are. I have one project I work on as a hobby when I have free time, I'd also like to showcase it. Maybe a projects section with "show more?". Definitely need an "about me" section, contact section. I have an avatar so maybe we'll put it in the "about me" section. I'm not connecting email, my email will be hardcoded. I have an article on Medium, dev, we'll put links to those platforms. We'll also link my LinkedIn. Additionally, we'll add a blog section. I value minimalism and simplicity, I'd like to keep the site in that style. I'd like the project to follow the latest web development standards, both on small and large screens.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even though there's not much text, this is a good enough introduction to start Spec Driven Development, which means directing AI to achieve the results we want.&lt;/p&gt;

&lt;p&gt;Finally, we'll have 4 files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;requirements.md → project requirements (WHAT we're building)&lt;/li&gt;
&lt;li&gt;plan.md → architecture plan (HOW we're building)&lt;/li&gt;
&lt;li&gt;tasks.md → list of tasks to complete (what SPECIFICALLY to do)&lt;/li&gt;
&lt;li&gt;STATUS.md → current project status (WHERE we are)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The structure can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/
    ├── requirements.md
    ├── plan.md
    ├── tasks.md
    ├── STATUS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having our notes, we can move to the first &lt;code&gt;.md&lt;/code&gt; file. This format is AI-friendly, it's the standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Developing requirements with AI&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Time to transform our rough notes into structured specs. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;WHAT are we building? — requirements.md file&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;We already know roughly what we want to achieve. Time to write the requirements file requirements.md.&lt;/p&gt;

&lt;p&gt;This will be a file informing AI why the project is being created. We won't write it manually. We launch Claude Code typing something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Using my initial plan  here our previously prepared notes  help me prepare the requirements file requirements.md. Then we'll work on the plan, later we'll define the list of tasks to complete, but for now let's focus only on writing down requirements. At the end we'll save it to a file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude Code will think, might ask some questions, it's worth having a discussion. Here I thought I'd add dark mode and after deeper discussion with AI we concluded that the blog section is unnecessary (since there will be links to Medium and Dev.to). Here's the final requirements.md file:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ke3w7fonvs95gx1rp8m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ke3w7fonvs95gx1rp8m.png" alt="Example requirements.md file with problem statement and user stories" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;HOW are we building? — plan.md file&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;We (me and AI 😛) already know what we're building, now we need to define how. We need to choose the tech stack we'll build in. The site is very simple so we don't need to overcomplicate and we can discuss with AI a stack that fits our requirements. My choice was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js because it has good documentation and you can host it for free on the Vercel platform&lt;/li&gt;
&lt;li&gt;for styling Tailwind CSS + shadcn/ui to use ready-made components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After choosing the stack, we can ask AI to research what architecture is worth applying in a portfolio project. Simple project, so the architecture doesn't have to be fancy.&lt;/p&gt;

&lt;p&gt;We already have the stack and architecture. We'll host the app on Vercel because it's friendly for Next.js. The domain remains. Half the internet runs on Cloudflare and they have cheap domains per year (about $11/year) → so I decided to buy it there later. Let's move to tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What SPECIFICALLY to do? — tasks.md&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Just as we generated a plan based on requirements, we generate tasks based on the plan. We can write in Claude Code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Review the files @requirements.md and @plan.md and then prepare a tasks.md file that will contain a list of tasks to complete. Tasks should be arranged in a sensible TODO. Spend time as an analyst and think about the tasks. Use ultrathink mode&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI will think and as a result generate a pretty good action plan. If we remember something, we can extend it. We have tasks! Time to track progress.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;WHERE are we → STATUS.md&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Context is the biggest pain point of working with AI. You want to work so AI knows what you're thinking about and has access to needed information. That's exactly what the STATUS.md file is for - it informs AI what stage you're at. By design, I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;STATUS.md file to edit automatically after a series of tasks&lt;/li&gt;
&lt;li&gt;after opening a new session and asking Claude Code "where did we leave off?" it would answer based on the STATUS.md file and we'd continue work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be safe, before the end of a session I asked Claude "Should we update the STATUS.md file?" It happened that it forgot, so I asked.&lt;/p&gt;

&lt;p&gt;Claude Code is your friend. Don't know how to prepare STATUS.md file? Ask AI 😈&lt;/p&gt;

&lt;p&gt;You can for example type:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Review the files @requirements.md, @plan.md, and @tasks.md, then come up with a progress tracking system. I suggest a STATUS.md file that you'll automatically update after each task. I want to achieve such a result that when I close the Claude Code session and open a new one I'll ask "Where did we leave off?" and you'll know! Don't overcomplicate, the project is small. Review the structure, let's not over-engineer. Ultrathink on this&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few questions and you'll come up with something great. So we have it, we have mini Spec Driven Development. Below is the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwhh0vyizolzsua16nf0k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwhh0vyizolzsua16nf0k.png" alt="STATUS.md file showing completed project status" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Final docs touches before work&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;At this stage it's worth running the &lt;strong&gt;/init&lt;/strong&gt; command in Claude Code. It will make Claude get familiar with the project and generate CLAUDE.md, which it always has access to, it's in its context. Running the command looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb775xll24kzgh2r64cdd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb775xll24kzgh2r64cdd.png" alt="Claude Code /init command interface" width="800" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After generating CLAUDE.md let's also type:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Review my work workflow based on @requirements.md, @plan.md, @tasks.md and STATUS.md → make the task tracking system aligned with CLAUDE.md. I don't want to remind you every time about saving progress and where to get the status from, so adjust CLAUDE.md to my workflow. Before closing a session I want to have an up-to-date STATUS.md, and when opening a new one I want you to always review STATUS.md so you can answer the question "What will we be working on now?" Ultrathink on this&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a very important element for our workflow. We want Claude Code to have the most current project state.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Development&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Only at this stage do we connect GitHub, upload the project and start coding. Actually, we ask AI to do it 🙏. I manually pushed the code to GitHub, and then asked Claude Code to use GitHub CLI and make commits. In my case, the first commit to GitHub had several thousand lines of text, not code but just .md files. Well-coordinated specs will make AI handle things great. Remember to commit quite often, preferably after each bigger task.&lt;/p&gt;

&lt;p&gt;From this point on, it gets much easier. If you've made it through all this, you'll see how smoothly it'll go now. We navigate AI to go through tasks. You'll probably get a few errors, then ask AI to analyze. For example, I got an error that the Tailwind config file was created in v3, but the Tailwind styles themselves were already in v4. Due to this inconsistency, some styles didn't work. Going through tasks, you'll naturally decide that something is unnecessary, maybe you'll want to add something - that's normal, add it. For me, at the end of the process, I added a second language and another Tech Stack section. But it's best to finish the plan and then beautify.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Testing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After you go through all tasks, it's time for testing. What you don't like, screenshot it and upload to Claude Code asking for a fix, change. You can also ask for audits, for example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Analyze my site in terms of the latest UX/UI standards. Focus on refining existing elements instead of proposing new ones. Ultrathink on this&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You'll see how much such requests give. Don't save on iterations. Ask for different things.&lt;/p&gt;

&lt;p&gt;SEO standards, mobile standards, legal standards, performance standards. We don't need to worry too much about security because we have a static site without any integrations. AI is only as good as its user. It's worth asking open questions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do you see any elements we might have missed, maybe SEO, performance?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude knows a lot. Questions matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;MCP Playwright&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;During testing, mcp_playwright came in handy. In short, it did for me what you do manually - tested in Google browser. Usage is simple, first you install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude mcp add playwright npx @playwright/mcp@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installation you tell Claude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use mcp_playwright to test my site in terms of UX/UI. Suggest improvements if you notice elements that are unreadable or not working.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude automatically starts the server and tests:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0r3qtqxkjk8z0gj98ib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0r3qtqxkjk8z0gj98ib.png" alt="Claude Code using MCP Playwright for automated testing" width="800" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Lighthouse&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;There's a plugin to install in Google Chrome called Lighthouse. It lets you measure your site's performance and SEO. I recommend installing it and doing analytics. Copy the report, paste it into Claude Code and ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Review the Lighthouse report. Suggest improvements that will help raise the report quality so our site is as Lighthouse-friendly as possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The report generated via the plugin looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgbaqqb2m8zg1f9jamz1x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgbaqqb2m8zg1f9jamz1x.png" alt="Lighthouse scores: 99 Performance, 92 Accessibility, 100 Best Practices, 100 SEO" width="800" height="828"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also use Lighthouse CLI and ask Claude directly to conduct the analysis.&lt;/p&gt;

&lt;p&gt;I'd suggest using Lighthouse Chrome only when the site is running on Vercel. Otherwise, some errors might pop up.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Deployment&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After the phase of various tests and checking prompts, we're ready for deployment.&lt;/p&gt;

&lt;p&gt;For hosting we choose Vercel in the free hobby version.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Vercel — hosting&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Uploading is very simple. We create an account, connect with GitHub, choose the repo and click deploy. Done! The site is already running on a test domain from Vercel. This is what a successful deployment looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F95npocuml69f7zgzneh1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F95npocuml69f7zgzneh1.png" alt="Successful Vercel deployment showing Ready state" width="800" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's worth going to the Analytics tab and enabling tracking. Remember then to add a cookie consent banner - not every user gives consent. Tracking lets you measure the number of visits and country of origin.&lt;/p&gt;

&lt;p&gt;Enabling analytics in Vercel panel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1tjp615a6kntihbs3un.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1tjp615a6kntihbs3un.png" alt="Vercel Analytics panel with Enable button highlighted" width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, if we want to have our own domain, we'll need Cloudflare.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Cloudflare — domain&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;At this point, it's time to choose a domain name. You check availability on Cloudflare - if it's free, you click buy and go through payment.&lt;/p&gt;

&lt;p&gt;Domain search on Cloudflare:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6bxwap83e5638zps3acy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6bxwap83e5638zps3acy.png" alt="Cloudflare domain registration search showing domain already registered" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You have the domain, now it's time for DNS. In Cloudflare you set up records that will connect the domain with Vercel. Don't know how? Paste screenshots into Claude and ask step by step.&lt;/p&gt;

&lt;p&gt;Final DNS setup:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flgf545i99fyyeag604a2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flgf545i99fyyeag604a2.png" alt="Cloudflare DNS records configuration with A and CNAME records" width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DNS in Cloudflare ready, now we connect the domain in Vercel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9qdy803720vqol5ebkva.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9qdy803720vqol5ebkva.png" alt="Vercel domain settings showing Valid Configuration status" width="800" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now we have everything. Sometimes you need to wait up to 24 hours for DNS to propagate. When it does - done. That's basically the end. We have a project running on the internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Summary&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In the article above, I wanted to show what the process of creating a website by a non-technical person can look like. I believe that currently with AI we can do a lot, we don't need to limit ourselves. If previously setting up a site with hosting and domain could be overwhelming, now with AI support it's enough to ask the right questions, paste the right screenshots. Say where we are and what we want to achieve.&lt;/p&gt;

&lt;p&gt;This Spec Driven Development system I developed with Claude Code. It works well in small projects. As proof - my site was created exactly like this: &lt;a href="https://www.kamilbuksakowski.dev" rel="noopener noreferrer"&gt;https://www.kamilbuksakowski.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxnbzdr9b4phnek31ni6r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxnbzdr9b4phnek31ni6r.png" alt="Final live portfolio site with dark minimalist design" width="800" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm satisfied. I encourage you to experiment with Claude Code and don't be afraid of new topics. AI is a great teacher 🚀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>specdriverdevelopment</category>
      <category>webdev</category>
    </item>
    <item>
      <title>From Frustration to Automation: My 3-Month Journey with i18n Translations</title>
      <dc:creator>Kamil Buksakowski</dc:creator>
      <pubDate>Tue, 09 Dec 2025 07:00:25 +0000</pubDate>
      <link>https://dev.to/kamilbuksakowski/from-frustration-to-automation-my-3-month-journey-with-i18n-translations-3i0</link>
      <guid>https://dev.to/kamilbuksakowski/from-frustration-to-automation-my-3-month-journey-with-i18n-translations-3i0</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I optimized my translation writing process!&lt;/p&gt;

&lt;p&gt;Two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I type the error intention or literal error,&lt;/li&gt;
&lt;li&gt;I run the script.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Boom! That’s it, I just added 7 translations to JSON files. Commit and done. Notice I typed “intentions”, not literal translations 😈&lt;/p&gt;

&lt;h3&gt;
  
  
  How was it before?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Come up with a translation,&lt;/li&gt;
&lt;li&gt;Come up with a translation key,&lt;/li&gt;
&lt;li&gt;Translate to other languages,&lt;/li&gt;
&lt;li&gt;Add translation to JSON files,&lt;/li&gt;
&lt;li&gt;Replace the key in the code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As you can see, tons of work. But this was the problem that got me started on automating translations. You can read about my entire journey and the final version below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frustration and Problem Definition
&lt;/h2&gt;

&lt;p&gt;In the beginning, there was frustration. I was adding translations manually and getting annoyed that it was boring and a waste of time. I knew it could be faster, but how? I started by defining what I wanted to achieve and what I knew.&lt;/p&gt;

&lt;p&gt;What do I want to achieve? I want translations to be added with minimal effort.&lt;/p&gt;

&lt;p&gt;What do I know? I know the folder structure and that it’s JSON I want to fill with appropriate translations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;src/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;i&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="err"&gt;n/&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;de/&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;translation.json&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;🇩🇪&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;German&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;en/&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;translation.json&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;🇬🇧&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;English&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(source)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;es/&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;translation.json&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;🇪🇸&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Spanish&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fr/&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;translation.json&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;🇫🇷&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;French&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;it/&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;translation.json&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;🇮🇹&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Italian&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;nl/&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;translation.json&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;🇳🇱&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Dutch&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pl/&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;translation.json&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;🇵🇱&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Polish&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I know I want translations to work in two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I type the translation in code,&lt;/li&gt;
&lt;li&gt;AI replaces the key in code and adds appropriate translations to the right JSON file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing more. Knowing what I wanted to achieve, I was ready to take on the challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Encounter with AI, First Disappointment
&lt;/h2&gt;

&lt;p&gt;I started getting into AI and the process of writing code faster. So I started using Claude Code, and I was impressed by how well it handled code. Then it was time to tackle my problem — I asked it to add translations for 7 languages.&lt;/p&gt;

&lt;p&gt;It added the files, but the whole thing took over a minute and was very expensive (lots of tokens). If you didn’t clearly specify the rules for finding keys, it tried to read the entire translation JSON. Each JSON over 25k tokens and it didn’t even read the whole thing! Because there’s a 25k limit, then it chunks it. So after approving Claude Code’s operations and over a minute — it worked. I didn’t like the result: slow, expensive. I abandoned the topic for a few months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem That Wouldn’t Go Away. Custom Commands in Action
&lt;/h2&gt;

&lt;p&gt;This topic really bothered me! How to do it right, it must be possible, right? And that’s how I stumbled upon Custom Commands. Why tell the LLM what to do every time and which translations to add where and how, when I can create a command run from the Claude Code terminal that will have instructions.&lt;/p&gt;

&lt;p&gt;The result of my work was the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/i18n-extract &lt;span class="s2"&gt;"Translation here as argument"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Goal? I type the command, and as a result it should do all the steps I was doing manually.&lt;/p&gt;

&lt;p&gt;How did it go? Badly, on the plus side the rules were written in the i18n-extract.md file, but still: long, expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  EUREKA! What if I move some of the work to Bash?
&lt;/h2&gt;

&lt;p&gt;At some point I had a developer epiphany. I’ll tell Claude that what’s mechanical should be executed in Bash instead of doing it itself.&lt;/p&gt;

&lt;p&gt;I went back to the beginning: why do I need AI in translations? To do translations! So simple yet so easy to overcomplicate 😅&lt;/p&gt;

&lt;p&gt;I took this thought further and came to what I thought was a brilliantly simple division: mechanical layer and creative layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creative layer&lt;/strong&gt;: AI handles generating translations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mechanical layer&lt;/strong&gt;: Bash will handle all operations of finding translations to replace, saving to files, etc.&lt;/p&gt;

&lt;p&gt;After the dust of joy settled, the first problem appeared. How is Bash supposed to find the fragment in code that it needs to send to Claude Code? That was a tough question.&lt;/p&gt;

&lt;p&gt;Hmm, what would I actually like the full process to look like?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I type throw new Error(‘Email is already taken’),&lt;/li&gt;
&lt;li&gt;I run the command and done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then I remembered why I’m doing this too — to make it convenient for me, and since I’m building it for myself, I can set the rules to make it work.&lt;/p&gt;

&lt;h3&gt;
  
  
  The '#go' marker.
&lt;/h3&gt;

&lt;p&gt;A very simple concept that solved the problem of telling Bash where to find what it’s looking for in the codebase.&lt;/p&gt;

&lt;p&gt;At the end we simply add #go, to tell Bash “I’m here”. We assume there’s only one #go in the project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email is already taken #go&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;'#go', despite being simple, was a breakthrough for me. While working on this solution, or rather typing the error “Email is already taken #go”, I thought you could make a significant upgrade and a serious convenience boost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intention mode vs literal
&lt;/h3&gt;

&lt;p&gt;What’s wrong with “Email is already taken #go”?&lt;/p&gt;

&lt;p&gt;We have to type the error message! The literal one it should throw.&lt;/p&gt;

&lt;p&gt;What if we split this into two modes? &lt;strong&gt;Intention&lt;/strong&gt; mode and &lt;strong&gt;literal&lt;/strong&gt;. Literal would translate 1:1 what we typed, and intention mode would come up with the translation for us.&lt;/p&gt;

&lt;p&gt;Another problem: How to tell Bash when to activate intention mode and when literal? Keywords! How powerful they are! I adopted a simple assumption: if the error contains &lt;strong&gt;keywords&lt;/strong&gt; (“Need”, “Create”, “Generate”) it will activate intention mode and come up with a translation. Example of activating intention mode based on the word “need”:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Need error that user reached project limit on free plan and must upgrade to premium to add more #go&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thus keywords became responsible for activating intention mode (my favorite) or literal mode.&lt;/p&gt;

&lt;p&gt;Going through this long journey, after fine-tuning the Bash script to change, delete, replace what was needed — it was time for tests.&lt;/p&gt;

&lt;p&gt;Flow:&lt;/p&gt;

&lt;p&gt;1) I type the error intention in code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Need error that user reached project limit on free plan and must upgrade to premium to add more #go&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) I run the custom command from Claude Code terminal: /i18n-extract. As a result, it generated and populated translations to 7 files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;de/translation.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PROJECT_LIMIT_REACHED_UPGRADE_REQUIRED"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sie haben die maximale Anzahl an Projekten für Ihren aktuellen Free-Tarif erreicht. Um weitere Projekte hinzuzufügen und erweiterte Funktionen für Ihr Logistikmanagement freizuschalten, führen Sie bitte ein Upgrade auf unseren Premium-Tarif durch.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;en/translation.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PROJECT_LIMIT_REACHED_UPGRADE_REQUIRED"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You have reached the maximum number of projects allowed on your current Free plan. To add more projects and unlock advanced logistics management features, please upgrade to our Premium plan.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;es/translation.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PROJECT_LIMIT_REACHED_UPGRADE_REQUIRED"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ha alcanzado el número máximo de proyectos permitidos en su plan Free actual. Para añadir más proyectos y desbloquear funciones avanzadas de gestión logística, actualice a nuestro plan Premium.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fr/translation.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PROJECT_LIMIT_REACHED_UPGRADE_REQUIRED"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vous avez atteint le nombre maximum de projets autorisés dans votre offre Free actuelle. Pour ajouter davantage de projets et débloquer les fonctionnalités avancées de gestion logistique, veuillez passer à notre offre Premium.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;it/translation.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PROJECT_LIMIT_REACHED_UPGRADE_REQUIRED"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ha raggiunto il numero massimo di progetti consentiti nel Suo piano Free attuale. Per aggiungere ulteriori progetti e sbloccare le funzionalità avanzate di gestione logistica, effettui l'upgrade al nostro piano Premium.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;nl/translation.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PROJECT_LIMIT_REACHED_UPGRADE_REQUIRED"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"U heeft het maximale aantal projecten bereikt dat is toegestaan in uw huidige Free-abonnement. Om meer projecten toe te voegen en geavanceerde logistieke managementfuncties te ontgrendelen, kunt u upgraden naar ons Premium-abonnement.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pl/translation.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PROJECT_LIMIT_REACHED_UPGRADE_REQUIRED"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Osiągnięto maksymalną liczbę projektów dostępną w ramach aktualnego planu Free. Aby dodać więcej projektów i uzyskać dostęp do zaawansowanych funkcji zarządzania logistyką, prosimy o przejście na plan Premium.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, AI replaced the error in code with the i18n key.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Need error that user reached project limit on free plan and must upgrade to premium to add more #go&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NotFoundException&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;translation.PROJECT_LIMIT_REACHED_UPGRADE_REQUIRED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quality of translations was good. The whole thing took about 20 seconds with a stopwatch in hand, in intention mode. I had mixed feelings, I felt it could be better and faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  EUREKA x 2 — pure Bash!
&lt;/h2&gt;

&lt;p&gt;Better, better. How to do it better, what’s wrong, why so many thoughts when the translation process itself only takes a few seconds?&lt;/p&gt;

&lt;p&gt;The answer was interesting to me. It turned out that most of the time was spent on process orchestration through Claude Code. In other words, when I executed the command, Claude Code was deciding that Bash needed to run, then again, then again and again. But why? Why does Claude need to decide this when we know the full flow. Here it was worth going back to the earlier thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why do I need AI in translations? To do translations!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I took it literally. I refined the previously established architectural division:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Claude Code should only come up with the key and translations. It shouldn’t direct the process. It should be responsible for the creative part.&lt;/li&gt;
&lt;li&gt;Bash should do everything else. It directs the entire process and at the right moment makes an API Call to Claude to generate translations and that’s it! It’s responsible for the mechanical layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What needed to be done? Remove the command run from Claude Code CLI level and instead run a script written in Bash, which works on our assumptions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;finds the fragment in code we want to translate using the #go marker,&lt;/li&gt;
&lt;li&gt;determines whether to use intention or literal mode based on keywords contained in the error,&lt;/li&gt;
&lt;li&gt;API Call to Claude to translate,&lt;/li&gt;
&lt;li&gt;execution of the mechanical part by Bash (replacing translations, changing keys, cleanup, etc.).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Real intention mode flow:&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;1) I type the error intention in code that I want to throw (contains the keyword “Need”).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Need error that user reached project limit on free plan and must upgrade to premium to add more #go&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) I run Bash using “time” to immediately measure time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;time &lt;/span&gt;bash .claude/scripts/i18n-extract-full.sh &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) The process executes.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff55kisz34sjoyvi3hpns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff55kisz34sjoyvi3hpns.png" alt="Terminal output: 769 tokens used, 7 languages translated in 8.9 seconds" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Terminal output: 769 tokens used, 7 languages translated in 8.9 seconds&lt;/p&gt;

&lt;p&gt;On the screen we see details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total of 769 tokens used (only key and translation generation),&lt;/li&gt;
&lt;li&gt;translations added to 7 languages,&lt;/li&gt;
&lt;li&gt;the whole thing took 8.9 seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4) Done, 7 translations added to respective JSONs and key replaced, ready to commit.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Real literal mode flow:&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;1) I type the error in code 1:1 as it should be translated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email is already taken #go&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) I run Bash.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;time &lt;/span&gt;bash .claude/scripts/i18n-extract-full.sh &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) The process executes.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faaq4zrv7g2l0eth2en11.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faaq4zrv7g2l0eth2en11.png" alt="Terminal output: 340 tokens used, 7 languages translated in 3.8 seconds" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Terminal output: 340 tokens used, 7 languages translated in 3.8 seconds&lt;/p&gt;

&lt;p&gt;On the screen we see details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total of 340 tokens used,&lt;/li&gt;
&lt;li&gt;translations added to 7 languages,&lt;/li&gt;
&lt;li&gt;the whole thing took about 3.8 seconds for literal mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4) Done, ready to commit.&lt;/p&gt;

&lt;p&gt;Since the literal flow is much simpler, we got down to 3.8 seconds and minimal token usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;p&gt;As we’ve proven — custom commands don’t work well for this type of task. Below I’m attaching a comparison that clearly shows that for the type of problem discussed in the article, Bash wins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw5lqlh8el1a792v3sh9l.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw5lqlh8el1a792v3sh9l.jpg" alt="Comparison table: Custom command 20s vs Plain Bash 3.8–8.9s, showing 2–5x speed improvement and 10x fewer LLM calls" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Comparison table: Custom command 20s vs Plain Bash 3.8–8.9s, showing 2–5x speed improvement and 10x fewer LLM calls&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Summary&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This article was created to share the evolution of a solution to a specific problem. We often start with a complicated version, and through successive iterations we arrive at a simple solution. Simplicity is a form of art.&lt;/p&gt;

&lt;p&gt;The AI hype sometimes makes you tend to overcomplicate. That’s how I was at the beginning. I started with the “everything on AI side” solution, then “custom commands”, then “custom commands + Bash” and only at the end “Full Bash”. I wonder if there hadn’t been an AI hype, would I have started digging into Bash right away 🤔&lt;/p&gt;

&lt;p&gt;The solution shown in the article is part of my custom workflow, saves a significant amount of time and frustration, and was interesting to build. Below I’m recording the biggest EUREKA moments I experienced and conclusions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you’re in the driver’s seat → the #go marker solved a lot of problems, let’s be creative,&lt;/li&gt;
&lt;li&gt;just do it, even if the first solution will be overcomplicated, slow, you won’t use it. From idea to result can take a long time, it took me about 3 months for the thought to germinate and evolve during the process,&lt;/li&gt;
&lt;li&gt;orchestration on the Bash side + LLM only for the creative layer. Clear division that simplifies a lot,&lt;/li&gt;
&lt;li&gt;with custom workflow don’t be afraid to use keywords like “need” to trigger creative mode — ultimately it’s a solution for us, it’s important that it works and you want to use it, it should be convenient.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you if you made it this far! What automations have you managed to create? Maybe you have practical use cases for custom commands?&lt;/p&gt;

&lt;p&gt;Feel free to discuss in the comments 👇&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>automation</category>
      <category>programming</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
