<?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: Vladyslav Diachuk</title>
    <description>The latest articles on DEV Community by Vladyslav Diachuk (@vldi01).</description>
    <link>https://dev.to/vldi01</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%2F3705059%2F537bb30d-f00d-4c8f-b037-624a1bfbb5cd.png</url>
      <title>DEV Community: Vladyslav Diachuk</title>
      <link>https://dev.to/vldi01</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vldi01"/>
    <language>en</language>
    <item>
      <title>Modern KMP (Part 1): The End of the "404 Not Found"2</title>
      <dc:creator>Vladyslav Diachuk</dc:creator>
      <pubDate>Sun, 11 Jan 2026 10:26:55 +0000</pubDate>
      <link>https://dev.to/vldi01/modern-kmp-part-1-the-end-of-the-404-not-found2-39ca</link>
      <guid>https://dev.to/vldi01/modern-kmp-part-1-the-end-of-the-404-not-found2-39ca</guid>
      <description>&lt;p&gt;&lt;strong&gt;How to use "Contract-First" development to guarantee your Android/iOS clients and Ktor Backend are always in perfect sync.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s 4:55 PM on a Friday. You just deployed a hotfix to the backend. You feel good. Five minutes later, Sentry alerts start screaming.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;SerializationException: Field 'user_id' is required but missing.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ah, right. You refactored the backend response model to use camelCase &lt;code&gt;userId&lt;/code&gt;, but you forgot to update the Android &lt;code&gt;Retrofit&lt;/code&gt; interface and the iOS &lt;code&gt;Codable&lt;/code&gt; struct.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;"The Drift."&lt;/strong&gt; It is the silent killer of mobile agility.&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%2F5q8ntylhul56vedm0d9j.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%2F5q8ntylhul56vedm0d9j.png" alt="A split graphic showing a Client expecting  raw `Shape: Square` endraw  and Server sending  raw `Shape: Circle` endraw . Representing the API Drift" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the traditional world, we fight The Drift with communication: Slack messages, Swagger/OpenAPI docs, and hope. But docs get outdated, and hope is not a strategy.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;Kotlin Multiplatform (KMP)&lt;/strong&gt; world, we can do better. We can fight The Drift with the &lt;strong&gt;Compiler&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this 3-part series on &lt;strong&gt;Modern KMP Architecture&lt;/strong&gt;, I’m going to share the setup I use to bridge the gap between Client and Server.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Part 1 (This Article):&lt;/strong&gt; Full-Stack Type Safety (The "Contract-First" Pattern).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Part 2:&lt;/strong&gt; Scalable Modularization (Solving the "God Module" build problem).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Part 3:&lt;/strong&gt; The Anatomy of a Feature (Decoupled Navigation &amp;amp; UDF).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A Note on "Enterprise Grade" vs. "Experimental Magic":&lt;/strong&gt; The modularization strategies I will cover in Part 2 and 3 are battle-tested patterns I’ve learned from working on large-scale enterprise apps. However, the Server-Side generation I am showing today is a &lt;strong&gt;proof-of-concept tool&lt;/strong&gt;. It is magical, it works, but if you are building a banking backend, you might want to stick to manual routing while keeping the shared interface concept.&lt;/p&gt;

&lt;p&gt;Let’s solve the biggest problem first: &lt;strong&gt;The Contract.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Code-First" Contract
&lt;/h2&gt;

&lt;p&gt;Most KMP tutorials teach you how to share &lt;em&gt;Data&lt;/em&gt; (e.g., the &lt;code&gt;User&lt;/code&gt; data class). That's a good start, but it's not enough. If your Client thinks the endpoint is &lt;code&gt;GET /user&lt;/code&gt; and your Server thinks it's &lt;code&gt;GET /users&lt;/code&gt;, sharing the data class won't save you.&lt;/p&gt;

&lt;p&gt;We need to share the &lt;strong&gt;Behavior&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In my architecture, the "Source of Truth" is not a YAML file or a Confluence page. It is a simple Kotlin Interface located in a shared &lt;code&gt;:network:api&lt;/code&gt; module.&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%2F4u5x4h9zs0np9j3s4839.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%2F4u5x4h9zs0np9j3s4839.png" alt="Diagram showing the  raw `:network:api` endraw  module sitting in the center, feeding into  raw `:client` endraw  (Android/iOS) and  raw `:server` endraw  (Ktor). The Interface is the heart." width="800" height="436"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// :network:api/src/commonMain/kotlin/UserApi.kt&lt;/span&gt;

&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;UserApi&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"users/{id}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;

    &lt;span class="nd"&gt;@POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Body&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;

    &lt;span class="nd"&gt;@NoAuth&lt;/span&gt;
    &lt;span class="nd"&gt;@GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"users/public"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getPublicInfo&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;

    &lt;span class="nd"&gt;@Multipart&lt;/span&gt;
    &lt;span class="nd"&gt;@POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"users/avatar"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;uploadAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Part&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"avatar"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PartData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;):&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;💡 Mental Model:&lt;/strong&gt; If you have used gRPC, this will feel familiar. We are effectively using Kotlin Interfaces as our &lt;code&gt;.proto&lt;/code&gt; files. This gives us the strict contract safety of RPC, but keeps the simplicity, tooling, and inspectability of standard HTTP/JSON.&lt;/p&gt;

&lt;p&gt;This looks like a standard Retrofit interface. But in this architecture, this file drives &lt;strong&gt;everything&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Client Side (Ktorfit)
&lt;/h3&gt;

&lt;p&gt;For the client (Android, iOS, Desktop), we use &lt;a href="https://foso.github.io/Ktorfit/" rel="noopener noreferrer"&gt;Ktorfit&lt;/a&gt;. It’s essentially "Retrofit for KMP."&lt;/p&gt;

&lt;p&gt;It looks at that interface and generates the Ktor Client implementation for us.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generated Client Code (Simplified)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="nc"&gt;UserApiImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserApi&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"users/$userId"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is standard practice. But now, let’s do something radical.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Server Side (The Experiment)
&lt;/h3&gt;

&lt;p&gt;Usually, Ktor server routing is written manually. This is where the drift happens. You type &lt;code&gt;post("auth/login")&lt;/code&gt; manually, and if you make a typo, you get a 404.&lt;/p&gt;

&lt;p&gt;To solve this, I wrote a custom &lt;strong&gt;KSP (Kotlin Symbol Processing)&lt;/strong&gt; processor for the Server. It reads the &lt;em&gt;exact same&lt;/em&gt; interface used by the client and generates the server routing entry points.&lt;/p&gt;

&lt;p&gt;It even handles &lt;strong&gt;Context Propagation&lt;/strong&gt;. One of the biggest challenges with shared interfaces is: "How do I get the HTTP Headers or the Auth Token inside my implementation if the interface function signature doesn't have a &lt;code&gt;call&lt;/code&gt; parameter?"&lt;/p&gt;

&lt;p&gt;My processor wraps the execution in a &lt;code&gt;CallContext&lt;/code&gt;, allowing you to access the raw Ktor &lt;code&gt;ApplicationCall&lt;/code&gt; and your typed JWT token from anywhere in your implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/src/.../UserApiImpl.kt&lt;/span&gt;

&lt;span class="c1"&gt;// 1. You implement the shared interface&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserApiImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userDao&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserDao&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserApi&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 2. Need the raw call or token? Use the context wrapper.&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getCallContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserToken&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; 

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jwtToken&lt;/span&gt; &lt;span class="c1"&gt;// Strongly typed Token object&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;call&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;      &lt;span class="c1"&gt;// Raw RoutingCall (headers, cookies, IP)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userDao&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUserById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;NotFoundException&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userDao&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Magic Trick: Coroutine Context
&lt;/h3&gt;

&lt;p&gt;You might be wondering: &lt;em&gt;"How does &lt;code&gt;getCallContext()&lt;/code&gt; find the call if I never passed it as an argument?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If we added &lt;code&gt;call: ApplicationCall&lt;/code&gt; to the interface methods, the Client would break (because the Client doesn't know what a Ktor Server &lt;code&gt;ApplicationCall&lt;/code&gt; is).&lt;/p&gt;

&lt;p&gt;The solution is &lt;strong&gt;Kotlin Coroutine Contexts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Since Ktor handles every request inside a coroutine, my processor generates code that injects the data into the &lt;code&gt;CoroutineContext&lt;/code&gt; scope just before your function runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Generated by the Processor (Simplified)&lt;/span&gt;
&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"auth/register"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RegisterRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="c1"&gt;// Parsed from JWT or Unit if @NoAuth&lt;/span&gt;

  &lt;span class="c1"&gt;// The Trick: We inject the context into the Coroutine Scope&lt;/span&gt;
  &lt;span class="nf"&gt;withContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CallContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;impl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows &lt;code&gt;getCallContext()&lt;/code&gt; to retrieve the data from the current scope, acting like a "Coroutine-Local" variable. This keeps your shared interface clean and platform-agnostic, while giving the server implementation full access to the HTTP context.&lt;/p&gt;

&lt;p&gt;Now, your Server Application code is dumb. It just binds the implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/src/Application.kt&lt;/span&gt;
&lt;span class="nf"&gt;routing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The compiler forces me to provide an implementation of UserApi&lt;/span&gt;
    &lt;span class="nf"&gt;bindUserApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserApiImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userDao&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The "Wow" Moment
&lt;/h2&gt;

&lt;p&gt;Because both sides rely on the generated code from the &lt;em&gt;same&lt;/em&gt; interface file, we achieve &lt;strong&gt;Full-Stack Type Safety.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is what happens if I decide to change &lt;code&gt;createUser(@Body user: User)&lt;/code&gt; to &lt;code&gt;createUser(@Body user: User, @Query("force") force: Boolean)&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;I change the Kotlin Interface in &lt;code&gt;:network:api&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I hit "Build."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Client Fails:&lt;/strong&gt; The Android codebase turns red because I’m passing the wrong arguments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Server Fails:&lt;/strong&gt; The Backend codebase turns red because the implementation class no longer overrides the interface correctly.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The build does not pass until both the Client and the Server agree on the new contract.&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%2Fk4v58a1gq8cnrey6kets.gif" 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%2Fk4v58a1gq8cnrey6kets.gif" alt="GIF showing the IDE Split Screen. Left side: Interface file being edited. Right side: Build Output window showing errors in both  raw `:composeApp` endraw  and  raw `:server` endraw  modules simultaneously." width="760" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reality Check (Trade-offs)
&lt;/h2&gt;

&lt;p&gt;This sounds like magic, but every architectural decision has a cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. You Own The Tooling&lt;/strong&gt; This KSP processor is a cool project, but it is &lt;strong&gt;not&lt;/strong&gt; a standard library maintained by JetBrains. It works for my template, but if Ktor changes its DSL drastically, or if you have complex routing needs (e.g., wildcards, regex paths) that the processor doesn't support, you will hit a wall.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Recommendation:&lt;/strong&gt; For a startup or a hobby project? This is a superpower. For a massive enterprise legacy migration? Use the &lt;strong&gt;Shared Interface&lt;/strong&gt; pattern, but maybe write the server-side routing manually to stay on the safe side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. The JWT Type Safety Gap&lt;/strong&gt; There is one specific edge case I couldn't solve purely at compile-time. While the &lt;em&gt;API Contract&lt;/em&gt; is fully typed, the &lt;strong&gt;Auth Token Type&lt;/strong&gt; is not strictly enforced by the compiler. If you define your interface to require an &lt;code&gt;AdminToken&lt;/code&gt;, but inside your implementation you ask for &lt;code&gt;getCallContext&amp;lt;UserToken&amp;gt;()&lt;/code&gt;, the code will compile. However, at runtime, the &lt;code&gt;CallContext&lt;/code&gt; will contain the wrong token type, leading to a crash or an error. This is a limitation of how contexts are propagated at runtime vs. generic erasure at compile time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Strict Contracts&lt;/strong&gt; You cannot "just code." You must define your data shape and interfaces &lt;em&gt;before&lt;/em&gt; writing UI or server logic. This slows down initial prototyping but significantly speeds up long-term maintenance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The "Lock-Step" Trap (Versioning)&lt;/strong&gt; The compiler guarantees that your &lt;em&gt;current&lt;/em&gt; App code matches your &lt;em&gt;current&lt;/em&gt; Server code. It does &lt;strong&gt;not&lt;/strong&gt; guarantee that the app installed on a user's phone (from three months ago) matches your new Server deployment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Rule:&lt;/strong&gt; If you change an interface method signature, you break old clients.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Never break; only append. If you need to change the logic for &lt;code&gt;getUser&lt;/code&gt;, do not modify the existing function. Create &lt;code&gt;getUserV2()&lt;/code&gt; in the interface, mark the old one as &lt;code&gt;@Deprecated&lt;/code&gt;, and let the compiler guide you to migrate the client code while keeping the old endpoint alive for legacy users.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Seeing is Believing
&lt;/h2&gt;

&lt;p&gt;I’ve open-sourced this entire setup in my template, &lt;strong&gt;ModernArchitecture&lt;/strong&gt;. It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The Shared &lt;code&gt;:network&lt;/code&gt; module.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Ktorfit setup for Clients.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Custom KSP Processor for the Server (with Context &amp;amp; Auth support).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can clone it, change an endpoint, and watch your project break (safely) in real-time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/vldi01/ModernKMPArchitecture" rel="noopener noreferrer"&gt;Link to GitHub repo&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;We have a secure, type-safe contract. But as your app grows to 50, 60, or 100 features, you hit the next major KMP bottleneck: &lt;strong&gt;Build Times.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How do we organize a massive KMP project so that changing one line of code doesn’t force us to recompile the entire world?&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, I’ll switch gears to the &lt;strong&gt;Battle-Tested Architecture&lt;/strong&gt;: The &lt;strong&gt;API/Impl Split Pattern&lt;/strong&gt; and the &lt;strong&gt;Database Inversion&lt;/strong&gt; technique that keeps our builds lightning fast.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>mobile</category>
      <category>kotlin</category>
      <category>api</category>
    </item>
  </channel>
</rss>
