<?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: Mohammed Samrose</title>
    <description>The latest articles on DEV Community by Mohammed Samrose (@mohammedsamrose).</description>
    <link>https://dev.to/mohammedsamrose</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3863339%2F36c6317c-f893-42d5-881f-f3137c0cd445.jpeg</url>
      <title>DEV Community: Mohammed Samrose</title>
      <link>https://dev.to/mohammedsamrose</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohammedsamrose"/>
    <language>en</language>
    <item>
      <title>Let the Model Read, Let the Code Convert: Building a Bill OCR for Nepal</title>
      <dc:creator>Mohammed Samrose</dc:creator>
      <pubDate>Sun, 15 Nov 2026 12:29:12 +0000</pubDate>
      <link>https://dev.to/mohammedsamrose/let-the-model-read-let-the-code-convert-building-a-bill-ocr-for-nepal-2nbm</link>
      <guid>https://dev.to/mohammedsamrose/let-the-model-read-let-the-code-convert-building-a-bill-ocr-for-nepal-2nbm</guid>
      <description>&lt;p&gt;&lt;em&gt;How I turned photos of Nepali paper bills into reviewable, exportable data with Next.js, Gemini and Prisma 8.&lt;/em&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flyyw4i7lavojtok7ep0h.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flyyw4i7lavojtok7ep0h.png" alt="Bill OCR" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;In Nepal, many small businesses still receive printed or handwritten bills. An accountant then types each one into Excel or accounting software: vendor, PAN number, date, items, VAT, total. It is slow, and one wrong digit in a total or a PAN can cause mismatches at tax filing time.&lt;/p&gt;

&lt;p&gt;I wanted to build something people would actually use, so I picked this problem: take a photo of a bill, get structured data back, let a human check it, and export it. The result is &lt;strong&gt;Bill OCR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Generic OCR tools don't fit well because Nepali bills mix Devanagari numerals (०१२३) with English digits, use Bikram Sambat (BS) dates like 2083/07/22, carry 9-digit PAN/VAT numbers, and come in every layout imaginable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one design rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The model reads. The code decides.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A vision model is excellent at looking at a messy photo and finding the vendor, the date and the total. It is much less trustworthy at arithmetic or calendar conversion, and you can't unit test it. So the model's only job is to copy what is printed. Everything after that (converting numerals, parsing amounts, validating PAN numbers, converting BS dates) is ordinary code with tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Upload
  → validate (type, size, file signature)
  → store the image
  → extract with Gemini (JSON, values exactly as printed)
  → validate the raw output with Zod
  → normalize in code and collect warnings
  → save as PENDING_REVIEW
  → human review → CONFIRMED → CSV export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Extraction
&lt;/h2&gt;

&lt;p&gt;The prompt asks for JSON only, with fixed keys, and sets rules that matter more than the wording of the task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copy values exactly as printed. Don't convert numerals or dates.&lt;/li&gt;
&lt;li&gt;If a field is missing or unreadable, return null. Never guess.&lt;/li&gt;
&lt;li&gt;Keep item names in their original language.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Never guess" is the important one. A null shows up in the review screen as an empty field. A confident wrong guess looks like a correct value.&lt;/p&gt;

&lt;p&gt;I validate the response with Zod, where every field is nullable, so one unreadable field doesn't throw away the whole bill:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nc"&gt;RawBillSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;vendor_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&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="nf"&gt;nullable&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="n"&gt;vendor_pan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&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="nf"&gt;nullable&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="n"&gt;bill_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&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="nf"&gt;nullable&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="n"&gt;bill_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&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="nf"&gt;nullable&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="n"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&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="nf"&gt;nullable&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="n"&gt;vat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&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="nf"&gt;nullable&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="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&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="nf"&gt;nullable&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="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&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="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&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="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&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="nf"&gt;nullable&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="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&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="nf"&gt;nullable&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="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&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="nf"&gt;nullable&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;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;Note that every value is a string, exactly as printed. Numbers come later.&lt;/p&gt;

&lt;p&gt;I built this on Gemini's free tier, which sometimes answers 503 "model is overloaded". So &lt;code&gt;extractBill&lt;/code&gt; retries with short waits and then falls through a list of models read from an environment variable. A request timeout and a fallback model stop a busy model from hanging the upload for over a minute (I saw 87 seconds once).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Normalization
&lt;/h2&gt;

&lt;p&gt;Now the deterministic part. Nepali numerals are a one-line map:&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="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;DIGITS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s1"&gt;'०'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'१'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'२'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'३'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'3'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'४'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'५'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'६'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'6'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'७'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'7'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'८'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'8'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'९'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'9'&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;toEnglishDigits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&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="o"&gt;-&lt;/span&gt;९&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;DIGITS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&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;Amounts like &lt;code&gt;रु. ४,८००/-&lt;/code&gt; become &lt;code&gt;4800&lt;/code&gt; after converting the digits and stripping currency markers and commas. Anything that doesn't parse as a number becomes null rather than NaN. PAN/VAT numbers must be exactly 9 digits or they are rejected.&lt;/p&gt;

&lt;p&gt;Dates were the most interesting part. A bill may carry a BS date (2083-07-22) or an AD date, and I need both stored. I convert with &lt;code&gt;nepali-date-converter&lt;/code&gt;, then validate by converting back:&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NepaliDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toJsDate&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="c1"&gt;// Round trip: if converting back gives a different BS date,&lt;/span&gt;
&lt;span class="c1"&gt;// the input was invalid (for example day 32 in a 30-day month)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NepaliDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'YYYY-MM-DD'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="n"&gt;bs&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="n"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;Two bugs I caught in review are worth sharing because they are easy to miss:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Overlapping year ranges.&lt;/strong&gt; I treated years 2000–2099 as BS and 1900–2100 as AD. An AD date like 2026-11-08 matched the BS branch and was read as BS year 2026, which is around 1969 AD. The ranges must not overlap. In practice bills are either around 2060 and later (BS) or 1990–2059 (AD).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time zones.&lt;/strong&gt; One branch produced a date at local midnight and the other at UTC midnight, so an export could show some dates a day early depending on the server's time zone. Store the calendar day as UTC midnight everywhere.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: Catching mistakes
&lt;/h2&gt;

&lt;p&gt;Even a good extraction misreads digits sometimes. Bills carry their own redundancy, so I use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;subtotal + VAT should equal the total&lt;/li&gt;
&lt;li&gt;the item amounts should add up to the subtotal&lt;/li&gt;
&lt;li&gt;quantity × rate should equal an item's amount&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A mismatch doesn't block anything. It becomes a warning on the review screen, because a bill's own printed numbers are sometimes wrong and the accountant should decide. Hard errors (no vendor, no total, malformed PAN or date) block confirming.&lt;/p&gt;

&lt;p&gt;The review screen shows the original image next to an editable form. The same validation function runs in the browser for live feedback and again on the server at confirm time, so the server never trusts the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Accounts and authorization
&lt;/h2&gt;

&lt;p&gt;A bill contains PAN numbers and amounts, so I treated access seriously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Better Auth&lt;/strong&gt; handles email and password, GitHub and Google, with email verification and password reset by a 6-digit code, and optional authenticator-app 2FA.&lt;/li&gt;
&lt;li&gt;Every query is scoped by the signed-in user's id, and every API route checks the session itself. The Next.js proxy file only does a quick redirect for signed-out visitors, and I don't count it as a security boundary.&lt;/li&gt;
&lt;li&gt;Enabling the email-OTP plugin also adds an endpoint that signs you in with just an emailed code, which would skip both the password and the 2FA. I block that endpoint, so codes are only used to verify an email or reset a password.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One practical note: Prisma ORM 8 is new, and at the time of writing Better Auth's official Prisma adapter doesn't support it yet, so I used a community adapter and am watching for the official one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I haven't done yet
&lt;/h2&gt;

&lt;p&gt;I haven't measured accuracy properly. The next step is collecting 20–30 real bills (printed, handwritten, crumpled, photographed in bad light), recording the correct answers, and reporting per-field accuracy, so every prompt change is measured instead of guessed. I also want unit tests for the normalization functions, especially BS/AD boundary dates.&lt;/p&gt;

&lt;p&gt;If you copy this approach, check Google's current terms on how content sent to Gemini's free tier may be used before you send real client data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Let the model do what it is good at (reading messy images) and keep conversion, validation and arithmetic in code you can test.&lt;/li&gt;
&lt;li&gt;Prefer null over a guess, and show the nulls to a human.&lt;/li&gt;
&lt;li&gt;Use the redundancy already in the document, like totals, to flag likely misreads.&lt;/li&gt;
&lt;li&gt;Treat the human review step as part of the product, not an afterthought.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you handle Nepali invoices or build document pipelines, I'd love your feedback: &lt;a href="https://bill.mohammedsamrose.com.np" rel="noopener noreferrer"&gt;Bill OCR&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a CLI to Scaffold Next.js Apps with Any Stack — No More Boilerplate Hell</title>
      <dc:creator>Mohammed Samrose</dc:creator>
      <pubDate>Mon, 06 Apr 2026 07:37:03 +0000</pubDate>
      <link>https://dev.to/mohammedsamrose/i-built-a-cli-to-scaffold-nextjs-apps-with-any-stack-no-more-boilerplate-hell-45c9</link>
      <guid>https://dev.to/mohammedsamrose/i-built-a-cli-to-scaffold-nextjs-apps-with-any-stack-no-more-boilerplate-hell-45c9</guid>
      <description>&lt;p&gt;Every time I started a new Next.js project, I found myself doing the same thing: installing Prisma, wiring up NextAuth, setting up Zustand, configuring tRPC, and copy-pasting Docker files from my last project. It's not hard work — it's &lt;em&gt;tedious&lt;/em&gt; work. And after doing it for the fifth time, I decided to stop.&lt;/p&gt;

&lt;p&gt;That's how &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/create-samrose-app" rel="noopener noreferrer"&gt;create-samrose-app&lt;/a&gt;&lt;/strong&gt; was born — an opinionated CLI that scaffolds a fully configured Next.js project with your exact stack choices, all in a single command.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with Boilerplate
&lt;/h2&gt;

&lt;p&gt;Most Next.js starters fall into one of two traps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Too opinionated&lt;/strong&gt; — you're locked into one ORM, one auth library, one everything. Swap anything out and you're on your own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too bare&lt;/strong&gt; — you get a blank canvas with zero integrations, and you're back to manually wiring things up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What I wanted was something in the middle: a CLI that &lt;em&gt;asks&lt;/em&gt; what I want, then builds it — correctly wired, production-ready, no leftover config to clean up.&lt;/p&gt;




&lt;h2&gt;
  
  
  What create-samrose-app Does
&lt;/h2&gt;

&lt;p&gt;Run a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-samrose-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you get an interactive prompt that walks you through your stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;? Project name › my-app
? Select ORM › Prisma
? Select Database › PostgreSQL
? Select Auth › NextAuth
? Select UI Library › shadcn/ui
? Select State Management › Zustand
? Select API Layer › tRPC
? Select Testing › Vitest
? Extras › Docker, GitHub Actions, Husky
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it. Your project is scaffolded, every piece talking to every other piece, ready for &lt;code&gt;npm run dev&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Supported
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ORM
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prisma&lt;/strong&gt; — schema-first, great DX, strong TypeScript support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drizzle&lt;/strong&gt; — lightweight, SQL-like syntax, gaining fast adoption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeORM&lt;/strong&gt; — decorator-based, familiar if you're coming from the Java/Spring world&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mongoose&lt;/strong&gt; — for MongoDB-native workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Database
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;SQLite&lt;/li&gt;
&lt;li&gt;MongoDB&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NextAuth&lt;/strong&gt; — the community standard for Next.js auth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clerk&lt;/strong&gt; — drop-in auth with UI components&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT&lt;/strong&gt; — roll your own, full control&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  State Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Zustand&lt;/li&gt;
&lt;li&gt;Redux&lt;/li&gt;
&lt;li&gt;Recoil&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  API Layer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;tRPC&lt;/strong&gt; — end-to-end typesafe APIs, no REST or GraphQL schema boilerplate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;oRPC&lt;/strong&gt; — a newer alternative worth watching&lt;/li&gt;
&lt;li&gt;GraphQL&lt;/li&gt;
&lt;li&gt;REST&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Testing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Jest&lt;/li&gt;
&lt;li&gt;Vitest&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Extras
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Docker — containerize from day one&lt;/li&gt;
&lt;li&gt;GitHub Actions — CI/CD out of the box&lt;/li&gt;
&lt;li&gt;Husky — pre-commit hooks for lint and type checks&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How It Works Under the Hood
&lt;/h2&gt;

&lt;p&gt;The CLI is built with &lt;strong&gt;Node.js&lt;/strong&gt; and uses a prompt-driven approach to collect your choices. Based on your selections, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copies a base Next.js template&lt;/li&gt;
&lt;li&gt;Conditionally injects the right dependencies into &lt;code&gt;package.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Scaffolds config files (e.g., &lt;code&gt;prisma/schema.prisma&lt;/code&gt;, &lt;code&gt;drizzle.config.ts&lt;/code&gt;, &lt;code&gt;auth.ts&lt;/code&gt;, &lt;code&gt;trpc/router.ts&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Wires integrations together — for example, if you pick tRPC + Prisma + PostgreSQL, the router already has a database client initialized&lt;/li&gt;
&lt;li&gt;Sets up extras like &lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;, and &lt;code&gt;.husky/pre-commit&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every combination is tested to work together — you're not getting a patchwork of copy-pasted snippets.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Real Example
&lt;/h2&gt;

&lt;p&gt;Say you pick: &lt;strong&gt;Drizzle + PostgreSQL + Clerk + tRPC + Zustand + Vitest + Docker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You'll get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-app/
├── src/
│   ├── app/              # Next.js App Router
│   ├── server/
│   │   ├── db/           # Drizzle schema + client
│   │   └── api/          # tRPC routers
│   ├── store/            # Zustand stores
│   └── middleware.ts     # Clerk auth middleware
├── drizzle.config.ts
├── .env.example          # All required env vars listed
├── Dockerfile
└── vitest.config.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.env.example&lt;/code&gt; is pre-populated with every variable your stack needs — no hunting through docs to figure out what's required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Use create-t3-app?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://create.t3.gg/" rel="noopener noreferrer"&gt;T3 Stack&lt;/a&gt; is excellent and was a big inspiration. But it's tightly coupled to its own set of choices (tRPC + Prisma + NextAuth + Tailwind). If you want Drizzle instead of Prisma, or Clerk instead of NextAuth, or Zustand instead of nothing — you're doing manual surgery.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;create-samrose-app&lt;/code&gt; is built around flexibility. Every layer of the stack is a choice, and every combination is first-class.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-samrose-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;📦 npm: &lt;a href="https://www.npmjs.com/package/create-samrose-app" rel="noopener noreferrer"&gt;npmjs.com/package/create-samrose-app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 Docs: &lt;a href="https://csa.mohammedsamrose.com.np" rel="noopener noreferrer"&gt;csa.mohammedsamrose.com.np&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 GitHub: &lt;a href="https://github.com/samrosemohammed/create-samrose-app" rel="noopener noreferrer"&gt;github.com/samrosemohammed/create-samrose-app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;There are a few things on the roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payload CMS&lt;/strong&gt; integration for headless CMS setups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turborepo&lt;/strong&gt; support for monorepo scaffolding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storybook&lt;/strong&gt; as an extras option&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;--yes&lt;/code&gt; flag for non-interactive scaffolding with defaults&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it and hit something broken, or have a stack combination you'd love to see supported, open an issue on GitHub. PRs are welcome too — the more combinations that get tested in the wild, the better.&lt;/p&gt;




&lt;p&gt;Building &lt;code&gt;create-samrose-app&lt;/code&gt; saved me hours on every new project. Hopefully it does the same for you.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>cli</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
