
Rust micro-experienced web engineer implements new features with Claude Code and achieves stable operation
This page has been translated by machine translation. View original
I'm Kanaya from the Service Development Division, Platform Business Division, Cloud Business Headquarters.
I'll look back on what I thought about and how I acted, since I used AI—mainly Claude Code—to develop new features adopting Rust, and have been able to operate them without any major issues.
About Me
I'm a web application engineer who mainly does frontend development with TypeScript, React, and similar technologies on a daily basis. I write backend code only occasionally, and my level with Rust was that I had touched it somewhat through self-study.
Development Period
With that background, I implemented the backend for a new feature of a certain product in Rust together with Claude Code. The concept started moving forward in the first half of 2025, and full-scale development began around June 2025.
At that time, Claude Code had just been officially released. It was a timing when agentic coding tools were starting to line up all at once, and I began using it while feeling my way through "how much can I leave to it."
From there, I repeated prototyping and course corrections, and released to production (GA) in February 2026. Since then, I've continued adding features and making improvements. Development has been going on for a little over a year, production operation for about six months, and so far no major incidents have occurred.
I did feel an atmosphere where it was considered virtuous to implement everything at once with vibe coding, but looking back now, I think I was proceeding quite conservatively. (The long timeline is also partly because I took parental leave partway through 👶)
Technology Selection
First, I decided to build serverless
The system I built has requirements for what's called real-time ETL—an API that aggregates data in S3 and returns it. The amount of data handled varies depending on conditions, and aggregation in the worst case required high memory processing. There are few endpoints, but the aggregation patterns are extremely numerous—that's the nature of it.
With a resident server, you'd need to keep a high-memory instance running to handle peak load. At our scale, where the number of requests isn't that high, Lambda with pay-per-use pricing seemed like it could significantly reduce computing costs even with high-memory settings. So first, I decided to build serverless.
However, serverless has its own particular pitfalls beyond cold starts—like Lambda's 15-minute execution time and API Gateway's 29-second integration timeout.
At the time, even while bouncing ideas off AI to think through the design, I couldn't see everything clearly, and I remember repeatedly running into pitfalls and then trial-and-erroring again.
(These days, having it do Fable or similar might mean that's no longer the case)
From a usability perspective I didn't want to compromise on performance, and I was also hearing here and there within the company that Rust and serverless seemed to be a good match, so in the end I decided to write it in Rust.
The other development members pushed me forward despite the challenging selection, and I'm very grateful for that.
As an aside, AWS's official documentation also introduces how to build Lambda functions in Rust, and I think it's no longer as niche an option as it used to be.
Also an article from our company's DevelopersIO:
Separating AWS concerns from implementation with axum
You can use Rust without a framework, but I adopted a framework called axum.
Using axum allows you to implement it as a normal web application, and when running on Lambda you just wrap it with an adapter.
Locally and in CI you can start it as a normal web server and hit it with actual HTTP for integration tests. It was because I wanted to avoid a state where it could only run and be verified on AWS.
Trade-offs
Adopting Rust had several costs, and I worked through each of them as I went.
Heavy builds in CI
Rust builds generally take time, and if left as-is, CI charges pile up subtly. For now I've mitigated this by adding build caches like sccache. Honestly, I'm partly relying on it with only a "I heard it makes things faster" level of understanding. I also considered separating builds and deployments to reuse artifacts, and mechanisms like cargo workspace, but since it hasn't become critically slow so far, I've managed to get by.
Covering the learning curve with AI
My design notes from the introduction stage also noted Rust's learning cost as a potential concern.
However, I personally wrote almost none of the code myself, and there's no doubt that being able to start writing in a language I had minimal experience with was because AI was leading the implementation. If this premise collapsed, the reasons for adopting it would change entirely.
On the other hand, I think the general web application design knowledge I already had proved useful.
For example, when I gave instructions from the perspective of how to implement what would be DI in other languages for Rust and axum, it would suggest an implementation using State. That's an area where I felt glad to have had that knowledge.
Serverless-specific constraints
During verification I hit the response time limit and heavy requests were returning 504, so I switched to Regional placement and raised the integration timeout limit. For the 6MB response size limit, I handled it by first writing large results to S3 and returning a signed URL (30 minutes), splitting between "return directly" and "have them download" based on size.
Reference articles:
Things I Thought About and Practiced with Stable Operation in Mind
I took several measures to continue operating calmly after release, having built something in a language I had minimal experience with. What's common to all of them is two things: "not relying too much on my own understanding," and "aligning as closely as possible with widely known approaches to create a foundation that both I and AI can work with."
Cover the language barrier by aligning with widely known designs
The scariest thing about a language you have minimal experience with is being unable to fully read the scope of impact of code you wrote (or that AI wrote). Rather than pushing through this with language proficiency, I compensated by aligning with general designs that also work in other languages. Rather than relying on language-specific practices, keeping it in widely known forms means I can read it with my existing knowledge, and AI can handle it with knowledge it's already trained on.
For example, I used a structure that separates layers by responsibility. It's the form known as layered architecture or clean architecture, where the presentation layer, application layer, domain layer, and infrastructure layer depend on each other unidirectionally. Since it's not a particular language's style, the same language can be used whether implementation is left to AI or reviewed by a human. Whether the layer dependencies are being maintained—"is upper-layer code directly referencing lower layers (for example, is application layer code directly importing the infrastructure layer)?"—I prepared a mechanism to verify this and included it as a point for AI review as well.
The handling of side effects follows the same thinking. I don't let lower-level logic directly call side effects like getting the current time or reading environment variables. Time is obtained only once at the entry point, passed down in a swappable form (a trait like Clock). This way, calls like Utc::now() don't scatter throughout the code, and the entry points for side effects can be traced from types. In tests, time can be replaced with a fixed value, so processing that depends on dates can be verified stably. This too is nothing special—it's just the general idea of injecting dependencies applied to Rust.
Rather than continuing to understand every corner of the internal implementation, I found it easier personally to align with these forms that work in any language and fix the boundaries and positions of side effects.
Solidifying external specifications with tests
The basis for trust also leans not just on understanding internal implementations, but on "behavior as seen from the outside."
Making integration tests thorough
I actually hit production-equivalent data with HTTP and assert on the returned content. The production-equivalent test data (Parquet files) is managed with Git LFS, allowing the API to be exercised end-to-end with input close to real data. I'm conscious of writing test cases in a form where a human reading them can understand what's being guaranteed. The rule is to always run tests when there's a large amount of changes, regardless of the expected scope of impact, and they're always included in verification before submitting a PR. It's quite reassuring.
I've also caught some edge case tests—for example, there's an implementation that leaves cached data in S3 when returning a large response, and for patterns like this I set a bool value like cacheHit: true in the response. These edge cases tend to get overlooked, so while it's a value not used in the actual application, I've devised this approach to strengthen test robustness.
OpenAPI definition snapshots
I maintain the external shape of the API as OpenAPI, comparing the definition generated from the implementation against the committed definition to detect unintended changes to external specifications. What I most want to avoid is the response shape quietly changing during a refactor or dependency update, so I've frozen the external specification itself in tests.
Continuously replacing the foundation
Rather than build and done, I continue to replace the foundations I depend on. It's broad work that requires many steps, but it's exactly where AI excels, so I made full use of it.
CDK → Terraform migration
I migrated the infrastructure definition. The trigger was supply chain concerns. Since TypeScript is almost my native language, looking back I think CDK had its advantages too, but currently it's running without issues.
This was before GA, so there weren't any particularly major stumbling blocks.
Bruno → Hurl migration
I also migrated the integration test runner. This too was examined with supply chain concerns as the trigger. (Because Bruno depends on Node)
Hurl is a single binary based on libcurl, with no transitive npm dependencies, and can be run directly in CI.
Bruno allowed writing JS for assertions so fine-grained verification was possible, but I aligned to a Hurl-based implementation. Since the writing style changes, verification was needed to ensure tests were working as intended, but I confirmed that equivalent tests were achievable before migrating. (This was quite a challenge)
MinIO → LocalStack → VersityGW migration
I've switched local S3-compatible storage in response to circumstances like commercialization.
None of these are flashy, but they're part of the unglamorous operation of keeping up with changes in dependencies' licenses and risks and swapping out foundations accordingly.
Because it's solidified, large rewrites are possible later
Since the external specifications were frozen with snapshots and integration tests were thorough, even after large internal rewrites I could immediately confirm nothing was broken. It was this peace of mind that let me take on larger refactors even after release.
Specifically, I redesigned the domain model. I'm likely to get pushback that I should have done it beforehand, but the aggregation patterns were more numerous than expected, and I decided it was better to reorganize. This happened after GA. I think the background was also that terms like "understanding debt" were starting to become popular.
Initially I just maintained the layer structure mentioned earlier, but every time I added functionality, the overall picture gradually became harder to see.
The layers are roughly separated, but what's playing what role inside them... nobody knows.
Since I'm leaving a wide scope to AI, I want to avoid a situation where the human side loses its grasp of the overall picture.
Putting operations into words
Practices that would have been implicitly aligned in a human team aren't shared with AI unless put into words, as the scope left to AI expands. I tried to put decisions that had been handled verbally or mentally into writing as much as possible. Specifically, I write decisions in CLAUDE.md and .claude/rules, and coding conventions are also written out. These weren't prepared from the start—they've been gradually added to while operating.
However, writing things down doesn't mean AI will comply. It ignores them without hesitation. So putting things into words is only for aligning expectations, and for things I truly want enforced, rather than counting on AI compliance, I lean toward stopping them mechanically. The grep check for layer dependencies mentioned earlier, and the snapshot of external specifications (OpenAPI), are also examples of mechanically stopping things with the same thinking. Operationally, I also have a gate through the procedure (pre-pr) of running lint and tests before submitting a PR, and merge operations are done by humans—ultimately stopping things with mechanisms and humans. For PR descriptions, since they tend to become overly detailed if left alone, I explicitly specify writing them concisely.
One more thing: PRs related to AI are labeled ai-config. It's still effective today, serving as a marker to look back on what I've entrusted to AI and how. (I'm looking at this now while writing this article)
"The scope that can be left to AI" versus "the scope where humans stop things at the end"—drawing this line and turning it into decisions and mechanisms with each failure. This scaffolding work itself may have been the task where I moved my hands the most throughout development.
Summary
Even with minimal experience in a language, if you align with widely known designs to keep the scope of impact traceable, solidify external specifications with tests, and stop key points with mechanisms and humans rather than counting on AI compliance, you can create a state where you can continue operating in production.
The momentum of building everything at once is appealing, but I think the reason I've been able to operate this calmly is because I steadily accumulated unglamorous, conservative measures.