<?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>Forem: Dmitrii Verbetchii</title>
    <description>The latest articles on Forem by Dmitrii Verbetchii (@dmitrii-verbetchii).</description>
    <link>https://forem.com/dmitrii-verbetchii</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%2F3705611%2F25974a17-a77e-489a-ba12-93a7bee30b72.jpg</url>
      <title>Forem: Dmitrii Verbetchii</title>
      <link>https://forem.com/dmitrii-verbetchii</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/dmitrii-verbetchii"/>
    <language>en</language>
    <item>
      <title>API First in Practice: How We Made Frontend Types Predictable and Stable</title>
      <dc:creator>Dmitrii Verbetchii</dc:creator>
      <pubDate>Sun, 11 Jan 2026 21:09:09 +0000</pubDate>
      <link>https://forem.com/dmitrii-verbetchii/api-first-in-practice-how-we-made-frontend-types-predictable-and-stable-332c</link>
      <guid>https://forem.com/dmitrii-verbetchii/api-first-in-practice-how-we-made-frontend-types-predictable-and-stable-332c</guid>
      <description>&lt;p&gt;In many teams, frontend and backend evolve in parallel — and very often, they drift apart.&lt;br&gt;
I’ve seen this happen multiple times: API changes, frontend assumptions stay the same, and bugs appear only after the release.&lt;/p&gt;

&lt;p&gt;In our company, we decided to treat this problem as a process issue, not just a tooling one. The solution we landed on was an API First approach, where the frontend relies strictly on the API contract defined by the backend — nothing more, nothing less.&lt;/p&gt;

&lt;p&gt;This article is about how we implemented this approach in practice and what it changed for our frontend development.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem: Frontend–Backend Desynchronization
&lt;/h2&gt;

&lt;p&gt;Before adopting API First, our workflow looked familiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend changes an endpoint&lt;/li&gt;
&lt;li&gt;frontend still uses old assumptions&lt;/li&gt;
&lt;li&gt;manual TypeScript types become outdated&lt;/li&gt;
&lt;li&gt;bugs appear late — sometimes only in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even with good communication and documentation, the reality was simple:&lt;br&gt;
&lt;strong&gt;the frontend always found out about breaking changes too late.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TypeScript helps, but only if the types are accurate. And manually maintaining those types doesn’t scale.&lt;/p&gt;
&lt;h2&gt;
  
  
  Our Solution: API First as a Contract, Not Documentation
&lt;/h2&gt;

&lt;p&gt;In our team, API First means one simple rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The frontend uses only the TypeScript types and interfaces generated from the backend API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The OpenAPI (Swagger) schema is not &lt;em&gt;just documentation&lt;/em&gt;.&lt;br&gt;
It is the &lt;strong&gt;single source of truth.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If something is not described in the API contract, the frontend doesn’t assume it exists.&lt;/p&gt;
&lt;h2&gt;
  
  
  How It Works in Practice (CRUD User Example)
&lt;/h2&gt;

&lt;p&gt;Let’s take a simple CRUD example: &lt;strong&gt;users&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Before the backend implementation is even finished, the backend team provides us with an &lt;strong&gt;OpenAPI schema in YAML format&lt;/strong&gt; describing the future API.&lt;/p&gt;

&lt;p&gt;Here is a simplified but real example of such a schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openapi: 3.0.3
info:
  title: User Service API
  description: API for managing users
  version: "1.0"

paths:
  /users:
    get:
      tags:
        - users
      summary: Get list of users
      operationId: listUsers
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: status
          in: query
          schema:
            type: string
            enum: [active, inactive]
      responses:
        "200":
          description: List of users
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserListResponse"

    post:
      tags:
        - users
      summary: Create new user
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
      responses:
        "201":
          description: User created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

  /users/{id}:
    get:
      tags:
        - users
      summary: Get user by ID
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: User data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        status:
          type: string
          enum: [active, inactive]

    CreateUserRequest:
      type: object
      required:
        - name
        - email
      properties:
        name:
          type: string
        email:
          type: string
          format: email

    UserListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/User"
        total:
          type: integer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This YAML is enough for the frontend to start working — even if the backend implementation is still in progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating TypeScript Types and API Requests
&lt;/h2&gt;

&lt;p&gt;On the frontend, we use &lt;strong&gt;openapi-generator&lt;/strong&gt; to generate TypeScript code from this schema.&lt;br&gt;
The generator gives us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript types and interfaces&lt;/li&gt;
&lt;li&gt;API request functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We don’t write axios calls manually anymore.&lt;br&gt;
We don’t guess request shapes.&lt;br&gt;
We use only what was generated.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getUser&lt;/code&gt; clearly returns a User&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;createUser&lt;/code&gt; expects &lt;code&gt;CreateUserRequest&lt;/code&gt;
This makes the frontend code extremely predictable.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Generated Types as the Foundation of Frontend Code
&lt;/h2&gt;

&lt;p&gt;Once the types are generated, they are used everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API layer&lt;/li&gt;
&lt;li&gt;React components&lt;/li&gt;
&lt;li&gt;Forms&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;UI state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A very practical example is &lt;strong&gt;enums&lt;/strong&gt;.&lt;br&gt;
From the OpenAPI schema, we get a generated enum 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;export const UserStatusEnum = {
  Active: 'active',
  Inactive: 'inactive',
} as const;

export type UserStatusEnum =
  typeof UserStatusEnum[keyof typeof UserStatusEnum];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the frontend knows that &lt;code&gt;User.status&lt;/code&gt; can only be &lt;code&gt;"active"&lt;/code&gt; or &lt;code&gt;"inactive"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We use this enum to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build dropdown options&lt;/li&gt;
&lt;li&gt;create filters&lt;/li&gt;
&lt;li&gt;drive conditional UI logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No hardcoded strings.&lt;br&gt;
No duplicated constants.&lt;br&gt;
No silent mismatches.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI as an Early Warning System
&lt;/h2&gt;

&lt;p&gt;This approach really shines once CI is involved.&lt;/p&gt;

&lt;p&gt;Our process looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frontend regularly fetches the latest Swagger schema&lt;/li&gt;
&lt;li&gt;TypeScript types are regenerated&lt;/li&gt;
&lt;li&gt;CI runs &lt;code&gt;tsc&lt;/code&gt; and frontend unit tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the backend changes something unexpectedly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generated types change&lt;/li&gt;
&lt;li&gt;TypeScript compilation fails&lt;/li&gt;
&lt;li&gt;unit tests fail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is &lt;strong&gt;when&lt;/strong&gt; this happens.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Not after the release.&lt;br&gt;
Not in production.&lt;br&gt;
But during CI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means backend and frontend developers are aware of the problem immediately.&lt;br&gt;
Frontend tests become an additional safety net for backend changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Gained from This Approach
&lt;/h2&gt;

&lt;p&gt;After adopting API First, we noticed clear improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;predictable and stable frontend types&lt;/li&gt;
&lt;li&gt;faster development&lt;/li&gt;
&lt;li&gt;fewer questions between teams&lt;/li&gt;
&lt;li&gt;earlier detection of breaking changes&lt;/li&gt;
&lt;li&gt;much higher confidence when refactoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend stopped &lt;em&gt;guessing&lt;/em&gt; and started &lt;strong&gt;trusting the contract&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Introduce This in Another Team
&lt;/h2&gt;

&lt;p&gt;If you want to try this approach, my advice would be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with one service&lt;/li&gt;
&lt;li&gt;Generate types first, requests later if needed&lt;/li&gt;
&lt;li&gt;Enforce usage of generated types&lt;/li&gt;
&lt;li&gt;Add type checks to CI&lt;/li&gt;
&lt;li&gt;Treat OpenAPI as a contract, not documentation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is less about tools and more about discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;API First is not about Swagger files or generators.&lt;br&gt;
It’s about predictability.&lt;/p&gt;

&lt;p&gt;For a TypeScript frontend, having a reliable contract changes how you think about data.&lt;br&gt;
When the API is the source of truth, the frontend becomes simpler, safer, and more confident.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>react</category>
      <category>frontend</category>
      <category>openapi</category>
    </item>
  </channel>
</rss>
