<?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: Kshyatisekhar Panda</title>
    <description>The latest articles on DEV Community by Kshyatisekhar Panda (@kshyatisekhar_panda_a6076).</description>
    <link>https://dev.to/kshyatisekhar_panda_a6076</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%2F3866556%2F8f8f0602-9396-4cdd-9ca4-ad93090fa593.png</url>
      <title>DEV Community: Kshyatisekhar Panda</title>
      <link>https://dev.to/kshyatisekhar_panda_a6076</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kshyatisekhar_panda_a6076"/>
    <language>en</language>
    <item>
      <title>Stop Writing Types Twice: A Fullstack TypeScript Playbook</title>
      <dc:creator>Kshyatisekhar Panda</dc:creator>
      <pubDate>Wed, 08 Apr 2026 19:45:55 +0000</pubDate>
      <link>https://dev.to/kshyatisekhar_panda_a6076/stop-writing-types-twice-a-fullstack-typescript-playbook-3dch</link>
      <guid>https://dev.to/kshyatisekhar_panda_a6076/stop-writing-types-twice-a-fullstack-typescript-playbook-3dch</guid>
      <description>&lt;p&gt;If you're building &lt;strong&gt;fullstack with JavaScript (React on the front, Node.js on the back, TypeScript across both)&lt;/strong&gt; you've hit this wall before. You define an interface on the backend. You define the same interface on the frontend. They drift apart. Bugs happen. Nobody notices until production.&lt;/p&gt;

&lt;p&gt;This post walks through a pipeline where you define your data shape once as a &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; schema on the backend, and every other layer (API docs, frontend types, &lt;a href="https://tanstack.com/query/latest" rel="noopener noreferrer"&gt;React Query&lt;/a&gt; hooks) is generated from it automatically using &lt;a href="https://orval.dev/" rel="noopener noreferrer"&gt;Orval&lt;/a&gt;. Change the schema, regenerate, and TypeScript tells you exactly what broke. Across the entire stack.&lt;/p&gt;

&lt;p&gt;No shared packages to publish. No Swagger YAML to maintain by hand. No "hey can you update the frontend types" Slack messages.&lt;/p&gt;

&lt;p&gt;A quick note: we're still in the process of learning and fully adopting this flow ourselves. We haven't battle-tested every edge case, and our team is still building muscle memory around the workflow. But the early results have been promising enough that I wanted to share what we've seen so far. Think of this less as "here's the perfect setup" and more as "here's a path worth exploring if you're dealing with the same pain."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Flow
&lt;/h2&gt;

&lt;p&gt;Here's how a single type definition moves through each layer. Every arrow is automated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend&lt;/strong&gt; owns the data shape:&lt;br&gt;
SQL Query → Service Layer → &lt;strong&gt;&lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; Schema&lt;/strong&gt; → &lt;a href="https://www.openapis.org/" rel="noopener noreferrer"&gt;OpenAPI&lt;/a&gt; Spec (JSON)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt; consumes the contract:&lt;br&gt;
OpenAPI Spec → &lt;strong&gt;&lt;a href="https://orval.dev/" rel="noopener noreferrer"&gt;Orval&lt;/a&gt; codegen&lt;/strong&gt; → TypeScript interfaces + &lt;a href="https://tanstack.com/query/latest" rel="noopener noreferrer"&gt;React Query&lt;/a&gt; hooks + typed fetch calls → React Components&lt;/p&gt;

&lt;p&gt;The OpenAPI spec is the handshake between the two sides. Generated, not handwritten.&lt;/p&gt;
&lt;h2&gt;
  
  
  Backend: Define Once
&lt;/h2&gt;

&lt;p&gt;The backend team owns three things: talking to the database, defining the data shape, and producing the API contract.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Database Query
&lt;/h3&gt;

&lt;p&gt;You write SQL. You get rows with database-native column names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// comments.query.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET_COMMENTS_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  SELECT ID, TASK_ID, AUTHOR_ID, BODY_TEXT, CREATED_AT
  FROM TASK_COMMENTS
  WHERE TASK_ID = :p_task_id
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Service Layer
&lt;/h3&gt;

&lt;p&gt;Maps database naming to JavaScript naming. This is the only manual mapping in the entire pipeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// comments.service.ts&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nf"&gt;mapRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TASK_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AUTHOR_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BODY_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CREATED_AT&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;&lt;code&gt;BODY_TEXT&lt;/code&gt; becomes &lt;code&gt;body&lt;/code&gt;. This happens in one place. If the database column changes, this is the only line you update.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Zod Schema
&lt;/h3&gt;

&lt;p&gt;This is where one definition does three jobs at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// comments.schema.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ZCommentSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unique comment identifier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Comment text content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// TypeScript type derived, not duplicated&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Comment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;ZCommentSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Response wrapper&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ZGetCommentsResponseSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ZCommentSchema&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;What you get from this single definition:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime validation.&lt;/strong&gt; Bad data is rejected before it leaves your controller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript type.&lt;/strong&gt; &lt;code&gt;z.infer&amp;lt;&amp;gt;&lt;/code&gt; gives you the type. No separate interface needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API documentation.&lt;/strong&gt; Those &lt;code&gt;.meta()&lt;/code&gt; descriptions flow into the OpenAPI spec and eventually into the frontend dev's IDE tooltip.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Route Definition
&lt;/h3&gt;

&lt;p&gt;Your Express route wires the Zod schema into the API spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// swagger-specification.ts&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tasks/{taskId}/comments&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getComments&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;200&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ZGetCommentsResponseSchema&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;span class="p"&gt;},&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;
  
  
  OpenAPI Spec Generation
&lt;/h3&gt;

&lt;p&gt;A build script serializes everything into an OpenAPI JSON file. You never edit that JSON. It's an artifact.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn generate:openapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This JSON file is the contract. It's the only thing the frontend needs from the backend. No meetings, no Slack threads, no "what does this endpoint return?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend: Generate Everything
&lt;/h2&gt;

&lt;p&gt;The frontend team owns two things: generating typed hooks from the contract, and building the UI with them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orval Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://orval.dev/" rel="noopener noreferrer"&gt;Orval&lt;/a&gt; reads the backend's OpenAPI JSON and generates everything the frontend needs. It supports React Query, SWR, Angular, and plain axios/fetch out of the box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// orval.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../services/api/openapi-specs/api.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/api/gen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;override&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;mutator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/api/fetchInstance.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetchInstance&lt;/span&gt;&lt;span class="dl"&gt;'&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;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 Fetch Instance
&lt;/h3&gt;

&lt;p&gt;Your custom fetch wrapper. This is the only hand-written API code on the frontend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fetchInstance.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchInstance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;RequestInit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&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;
  
  
  Code Generation
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;yarn generate:specs&lt;/code&gt;. Orval produces a file you never touch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// gen/api.ts — AUTO-GENERATED, DO NOT EDIT&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GetComments200DataItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/** Unique comment identifier */&lt;/span&gt;
  &lt;span class="nl"&gt;commentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Comment text content */&lt;/span&gt;
  &lt;span class="nl"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useGetComments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&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;span class="nx"&gt;UseQueryResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GetComments200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ApiError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// fully wired TanStack Query hook&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You didn't write that interface. You didn't write that hook. They exist because the backend's Zod schema exists. The descriptions from &lt;code&gt;.meta()&lt;/code&gt; are now JSDoc comments. Hover in VS Code, there they are.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your React Component
&lt;/h3&gt;

&lt;p&gt;Just use the hook. Everything is typed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CommentsSection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isFetching&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetComments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ListItem&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commentId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ListItemText&lt;/span&gt;
            &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;secondary&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`User &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; · &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ListItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;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;Try typing &lt;code&gt;comment.&lt;/code&gt; and your IDE shows exactly what's available. Try &lt;code&gt;comment.bodyText&lt;/code&gt; and you get a red squiggly. Not a runtime bug. A compile-time error.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changes Feel Like
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Backend adds a field:&lt;/strong&gt; Add &lt;code&gt;editedAt&lt;/code&gt; to the Zod schema. The OpenAPI spec updates automatically. Tell the frontend team to regenerate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend regenerates:&lt;/strong&gt; Run &lt;code&gt;yarn generate:specs&lt;/code&gt;. The type now has &lt;code&gt;editedAt&lt;/code&gt;. IDE autocompletes it. No guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend renames a field:&lt;/strong&gt; Change &lt;code&gt;body&lt;/code&gt; to &lt;code&gt;content&lt;/code&gt; in the Zod schema. Frontend regenerates. TypeScript lights up every component that still says &lt;code&gt;.body&lt;/code&gt;. Fix them. Ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend removes a field:&lt;/strong&gt; Delete it from the schema. Frontend regenerates. TypeScript shows every line that references something that no longer exists.&lt;/p&gt;

&lt;p&gt;The compiler becomes the sync mechanism between teams. Not discipline. Not Slack. The compiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Breaks Down
&lt;/h2&gt;

&lt;p&gt;We're still early in adopting this, and we've already found the rough edges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DB to service mapping is still manual.&lt;/strong&gt; If someone renames a column in the database and doesn't update the service layer, you get &lt;code&gt;undefined&lt;/code&gt; at runtime. Zod doesn't know about your SQL. Integration tests against a real database are the only real safety net here. This is the weakest link in the chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fetch layer trusts the backend.&lt;/strong&gt; The &lt;code&gt;return data as T&lt;/code&gt; cast assumes the backend sent what the spec promises. If it didn't, TypeScript won't catch it. You can add Zod parsing on the response path too, but most teams skip it for performance. We're still debating whether to add it for development builds only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale generated code.&lt;/strong&gt; If the backend updates a schema and the frontend doesn't regenerate, types are out of sync. We added this to CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn generate:specs
git diff &lt;span class="nt"&gt;--exit-code&lt;/span&gt; src/api/gen/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build fails if generated code is out of date. This one we've already implemented, and it's caught things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team habits take time.&lt;/strong&gt; The tooling is the easy part. Getting everyone to trust the generated code, stop writing manual interfaces, and remember to regenerate. That's the real adoption curve. Some team members still instinctively create frontend types by hand. We have to remind ourselves to delete them and use the generated ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Need
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; + &lt;a href="https://github.com/samchungy/zod-openapi" rel="noopener noreferrer"&gt;zod-openapi&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Define schemas, convert to OpenAPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://orval.dev/" rel="noopener noreferrer"&gt;Orval&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Read OpenAPI, generate React Query hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://tanstack.com/query/latest" rel="noopener noreferrer"&gt;TanStack Query&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data fetching and caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript strict mode&lt;/td&gt;
&lt;td&gt;Makes the whole chain enforceable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're already using Zod and TanStack Query, you're halfway there. The gap is just adding &lt;a href="https://github.com/samchungy/zod-openapi" rel="noopener noreferrer"&gt;zod-openapi&lt;/a&gt; and &lt;a href="https://orval.dev/" rel="noopener noreferrer"&gt;Orval&lt;/a&gt;. Maybe half a day of setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Point
&lt;/h2&gt;

&lt;p&gt;After enough years of fullstack JavaScript, the pattern is always the same: the bugs that hurt don't come from complex logic. They come from two sides of the stack disagreeing about the shape of data.&lt;/p&gt;

&lt;p&gt;This pipeline draws a clean line. Backend owns the shape. Frontend consumes the shape. An OpenAPI spec is the handshake between them. Generated, not handwritten, never out of date.&lt;/p&gt;

&lt;p&gt;We're not going to pretend we've got it all figured out. We're still learning the workflow, still smoothing out the edges, still catching ourselves falling back to old habits. But the direction feels right. The few times the compiler has caught a type mismatch that would've been a runtime bug, those moments make the investment feel worth it.&lt;/p&gt;

&lt;p&gt;If you're dealing with the same frontend-backend sync headaches, this is worth a spike. Set it up on one endpoint. See how it feels. That's what we did, and we haven't looked back.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>react</category>
      <category>node</category>
    </item>
    <item>
      <title>Building My Portfolio with AI as a Pair Programmer</title>
      <dc:creator>Kshyatisekhar Panda</dc:creator>
      <pubDate>Tue, 07 Apr 2026 21:41:45 +0000</pubDate>
      <link>https://dev.to/kshyatisekhar_panda_a6076/building-my-portfolio-with-ai-as-a-pair-programmer-3hmd</link>
      <guid>https://dev.to/kshyatisekhar_panda_a6076/building-my-portfolio-with-ai-as-a-pair-programmer-3hmd</guid>
      <description>&lt;p&gt;I've been a full stack developer for over 7 years. I've built dashboards, APIs, real-time apps, and enterprise software. But I'd never built a proper portfolio site for myself. It was always on the list, never at the top.&lt;/p&gt;

&lt;p&gt;What changed was Claude Code. I wanted to try AI-assisted development on a real project, not a toy example. And I wanted to learn Astro at the same time. So I opened a terminal and started a conversation.&lt;/p&gt;

&lt;p&gt;This post is about what that experience was actually like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I started with an empty directory and a vague idea. I knew I wanted a dark-themed portfolio that showed my work, my career history, my cricket stats, and some travel photography. I didn't have a design in mind. I didn't have a Figma file. I just had a list of things I wanted on the page.&lt;/p&gt;

&lt;p&gt;I told Claude what I was looking for and it scaffolded an Astro project with TypeScript in strict mode. Within minutes I had a full project structure with components for each section: Hero, About, Skills, Projects, Beyond Code, Contact, and a Footer.&lt;/p&gt;

&lt;p&gt;That first version was fast. Maybe too fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Version Was Too Much
&lt;/h2&gt;

&lt;p&gt;The initial design had a lot going on. The hero section had a video background with a blur filter, a grain texture overlay, an accent gradient, and a dark overlay on top of all that. Every section had hover animations. There were four cards in a row that felt cramped on most screens.&lt;/p&gt;

&lt;p&gt;I looked at it and told Claude it felt like too much. That one comment changed the direction of the whole project. Claude agreed and suggested stripping the hero down to just video plus a dark overlay, changing the Beyond Code section to a 2x2 grid, and hiding the empty travel gallery until I had actual photos.&lt;/p&gt;

&lt;p&gt;This was the moment I realized what AI pair programming is really about. It's not about generating code. It's about having a conversation where you steer the direction and the AI handles the execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Emoji Rendering
&lt;/h2&gt;

&lt;p&gt;One of the more unexpected bugs was emoji rendering. I wanted emojis for the Beyond Code section. Cricket bat, football, airplane, video camera. Claude used unicode escape sequences like &lt;code&gt;\uD83C\uDFCF&lt;/code&gt; in the Astro template.&lt;/p&gt;

&lt;p&gt;They rendered as literal text. Instead of a cricket bat emoji, the page showed the string &lt;code&gt;\uD83C\uDFCF&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The issue was that Astro's HTML template doesn't evaluate unicode escapes the way JavaScript string literals do. The fix was simple: use actual emoji characters instead of escape sequences. But diagnosing it required understanding how Astro processes templates differently from JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extracting My LinkedIn Data
&lt;/h2&gt;

&lt;p&gt;I wanted my real career history on the site, not placeholder text. But LinkedIn blocks automated access. Every fetch attempt returned HTTP 999.&lt;/p&gt;

&lt;p&gt;So I saved the page source from my browser and gave it to Claude. The file was 1.3MB, two lines of heavily escaped React SSR data. Not exactly readable.&lt;/p&gt;

&lt;p&gt;Claude searched through the data using pattern matching. It found my job titles, company names, and education buried inside nested React component trees. Things like &lt;code&gt;"children":["Senior Analyst Programmer at ..."]&lt;/code&gt; hidden among thousands of lines of tracking data and UI component definitions.&lt;/p&gt;

&lt;p&gt;From that mess it extracted my complete career timeline: six roles across India and Sweden, from trainee to senior developer, plus early internships and my university education. All real, all accurate.&lt;/p&gt;

&lt;p&gt;That was genuinely impressive. I wouldn't have had the patience to parse that file manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Travel Photography
&lt;/h2&gt;

&lt;p&gt;I dropped some photos into the project directory. A shot of Trollstigen in Norway, two Northern Lights photos from Kiruna and Abisko, a Stockholm sunset, kayaks by the Norwegian fjords, and a winter campfire scene.&lt;/p&gt;

&lt;p&gt;One of the photos was a &lt;code&gt;.dng&lt;/code&gt; file (a RAW camera format). Claude converted it to JPEG using sharp-cli, then resized and optimized all six images from 21MB down to about 650KB total. It set up a gallery grid with hover overlays showing the location of each photo.&lt;/p&gt;

&lt;p&gt;When I told Claude the Northern Lights photos were from Kiruna and Abisko (not just "Scandinavia"), it updated the captions immediately. Small detail, but it matters. These are my photos and my memories. Getting the locations right matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Theme Toggle
&lt;/h2&gt;

&lt;p&gt;I mentioned wanting both dark and light themes. Claude added CSS custom properties for a light theme, a moon/sun toggle button in the navbar, localStorage persistence, and a script that runs before the page renders to prevent a flash of the wrong theme.&lt;/p&gt;

&lt;p&gt;It also respects the system preference on first visit. If your OS is set to light mode, the site starts in light mode. That kind of detail is easy to forget but makes a real difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Worked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt; The entire site went from empty directory to deployed on Vercel in one session. Components, styles, responsive design, animations, image optimization, blog setup with Astro content collections. All of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iteration.&lt;/strong&gt; The back and forth felt natural. I'd describe what I wanted, Claude would build it, I'd review and push back, and we'd converge on something good. It wasn't me accepting whatever was generated. It was a real collaboration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning Astro.&lt;/strong&gt; I came in knowing React and left understanding a fundamentally different approach to frontend development. Astro's component model, its zero-JS-by-default philosophy, content collections, and island architecture. I learned all of it by building, not by reading docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Didn't Work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;First drafts are generic.&lt;/strong&gt; Claude's initial text for my experience descriptions was made up. Generic lines like "Developed enterprise-grade software solutions" that could apply to anyone. I had to call that out and provide my real career data. AI can't know what you actually did at your job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unicode in Astro templates.&lt;/strong&gt; This one caught us both off guard. It's the kind of framework-specific gotcha that AI doesn't always know about until it fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You still need taste.&lt;/strong&gt; Claude will build whatever you ask for. If you ask for grain textures, blur filters, and gradient overlays all at once, you'll get exactly that. It takes a human to look at the result and say "this is too much."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Tell Other Developers
&lt;/h2&gt;

&lt;p&gt;Try it. Not as a replacement for knowing how to code, but as a way to move faster on things you already understand conceptually. I knew what I wanted in a portfolio. I knew what good frontend code looks like. Claude helped me get there without spending a week on CSS.&lt;/p&gt;

&lt;p&gt;Stay in the driver's seat. Review every line. Push back when something feels off. The best results came from me being specific about what I wanted and honest about what wasn't working.&lt;/p&gt;

&lt;p&gt;Use it to learn. I specifically chose Astro because I didn't know it. Having an AI that could scaffold, explain, and iterate on Astro code while I was learning it was genuinely useful. It's like having a coworker who already knows the framework you're picking up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;The site is live at &lt;a href="https://kshyatisekhar-panda.vercel.app" rel="noopener noreferrer"&gt;kshyatisekhar-panda.vercel.app&lt;/a&gt;. The code is on &lt;a href="https://github.com/kshyatisekhar-panda/about-me" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. It has a video hero, career timeline, skills grid, project showcase, cricket stats, travel photography, a resume viewer, a blog (you're reading it), and light/dark theme support.&lt;/p&gt;

&lt;p&gt;Every line of code was written through conversation. Not generated and forgotten, but discussed, reviewed, and refined.&lt;/p&gt;

&lt;p&gt;That's what AI pair programming looks like in practice.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>astro</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
