<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://beautyoncode.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://beautyoncode.com/" rel="alternate" type="text/html" /><updated>2026-05-19T05:38:04-04:00</updated><id>https://beautyoncode.com/feed.xml</id><title type="html">BeautyOnCode</title><subtitle>A flexible Jekyll theme for your blog or site with a minimalist aesthetic.</subtitle><author><name>Thanh Nguyen</name></author><entry><title type="html">Webhook Idempotency: Lessons from a ‘Double Charge’ Production Bug</title><link href="https://beautyoncode.com/backend/payment%20systems/webhook-idempotency-payment-systems/" rel="alternate" type="text/html" title="Webhook Idempotency: Lessons from a ‘Double Charge’ Production Bug" /><published>2025-11-05T00:00:00-05:00</published><updated>2025-11-05T00:00:00-05:00</updated><id>https://beautyoncode.com/backend/payment%20systems/webhook-idempotency-payment-systems</id><content type="html" xml:base="https://beautyoncode.com/backend/payment%20systems/webhook-idempotency-payment-systems/"><![CDATA[<p><img src="/assets/images/2026/05/2026-05-19-webhook-idempotency-cover.webp" alt="" /></p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#the-production-incident">The Production Incident</a></li>
  <li><a href="#understanding-webhook-delivery-guarantees">Understanding Webhook Delivery Guarantees</a></li>
  <li><a href="#what-is-idempotency">What is Idempotency?</a></li>
  <li><a href="#implementation-3-patterns-for-idempotency">Implementation: 3 Patterns</a></li>
  <li><a href="#critical-webhook-security">Webhook Security</a></li>
  <li><a href="#testing-idempotency">Testing Idempotency</a></li>
  <li><a href="#best-practices-checklist">Best Practices</a></li>
</ul>

<hr />

<h2 id="the-3-am-wake-up-call">The 3 AM Wake-Up Call</h2>

<p><em>Phone buzzes at 3 AM.</em></p>

<p><em>“Hey, merchants are reporting they’ve been charged payout fees twice. I checked and found several merchants with double charges…”</em></p>

<p>That message from our support team was my introduction to the harsh reality of webhook idempotency in payment systems.</p>

<p>Today I’m sharing the story of this production bug, why it happened, and more importantly - how to build systems that handle webhooks being sent “at least once” while ensuring your logic runs “exactly once.”</p>

<p>(Spoiler: It’s not as simple as checking the database.)</p>

<h2 id="the-production-incident">The Production Incident</h2>

<h3 id="the-context">The Context</h3>

<p>Our payment system integrates with a payment gateway (let’s call it “PaymentProvider”). Here’s how payouts worked:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Flow:
1. Merchant requests payout (withdraw money to bank)
2. PaymentProvider processes payout
3. PaymentProvider sends webhook: "Payout successful"
4. Our system charges 0.5% payout fee
5. Update merchant balance
</code></pre></div></div>

<h3 id="the-naive-implementation">The Naive Implementation</h3>

<p>Here’s what our code looked like initially:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Controller</span><span class="p">(</span><span class="dl">'</span><span class="s1">webhooks</span><span class="dl">'</span><span class="p">)</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">WebhookController</span> <span class="p">{</span>
  <span class="p">@</span><span class="nd">Post</span><span class="p">(</span><span class="dl">'</span><span class="s1">payment-provider</span><span class="dl">'</span><span class="p">)</span>
  <span class="k">async</span> <span class="nx">handlePayoutWebhook</span><span class="p">(@</span><span class="nd">Body</span><span class="p">()</span> <span class="nx">event</span><span class="p">:</span> <span class="nx">PayoutEvent</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">payout</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">payoutService</span><span class="p">.</span><span class="nx">findOne</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">payoutId</span><span class="p">);</span>

    <span class="c1">// ❌ What's wrong with this logic?</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">payout</span><span class="p">.</span><span class="nx">status</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">completed</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// Charge 0.5% fee</span>
      <span class="kd">const</span> <span class="nx">fee</span> <span class="o">=</span> <span class="nx">payout</span><span class="p">.</span><span class="nx">amount</span> <span class="o">*</span> <span class="mf">0.005</span><span class="p">;</span>
      <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">feeService</span><span class="p">.</span><span class="nx">createPayoutFee</span><span class="p">({</span>
        <span class="na">payoutId</span><span class="p">:</span> <span class="nx">payout</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
        <span class="na">amount</span><span class="p">:</span> <span class="nx">fee</span>
      <span class="p">});</span>

      <span class="c1">// Update status</span>
      <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">payoutService</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">payout</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">status</span><span class="p">:</span> <span class="dl">'</span><span class="s1">completed</span><span class="dl">'</span>
      <span class="p">});</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="p">{</span> <span class="na">received</span><span class="p">:</span> <span class="kc">true</span> <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Looks reasonable, right? We check the status before charging the fee. What could go wrong?</p>

<h3 id="the-incident-timeline">The Incident Timeline</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>10:15:00 - Merchant A requests payout of $10,000
10:15:30 - PaymentProvider processes payout, sends webhook #1
10:15:30 - Webhook #1 arrives at our server, starts processing
10:15:30 - Code checks status: 'pending' ✅
10:15:30 - Charges fee: $50
10:15:31 - PaymentProvider retries webhook (didn't get response in time)
10:15:31 - Webhook #2 arrives at our server
10:15:31 - Code checks status: still 'pending' ✅ (first request hasn't finished!)
10:15:31 - Charges ANOTHER fee: $50 ❌

Result: Merchant charged $100 instead of $50
</code></pre></div></div>

<h3 id="root-cause-analysis">Root Cause Analysis</h3>

<p><strong>Problem #1: Race Condition</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- Webhook #1 is processing (hasn't updated status yet)
- Webhook #2 arrives and checks status
- Both see status = 'pending'
- Both decide "haven't charged fee yet"
- Both charge the fee → DOUBLE CHARGE
</code></pre></div></div>

<p><strong>Problem #2: Webhook Delivery Semantics</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Payment providers guarantee "at-least-once delivery"
- They don't guarantee "exactly-once"
- Webhooks can be sent multiple times due to:
  → Network timeouts
  → Retry logic
  → Provider-side failures
  → Our server being slow to respond
</code></pre></div></div>

<p>This is not a bug in the payment provider - <strong>it’s by design</strong>.</p>

<h2 id="understanding-webhook-delivery-guarantees">Understanding Webhook Delivery Guarantees</h2>

<h3 id="three-types-of-delivery-semantics">Three Types of Delivery Semantics</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────┬──────────────────┬────────────────────┐
│   Type          │   Behavior       │   Real World       │
├─────────────────┼──────────────────┼────────────────────┤
│ At-most-once    │ Send once, no    │ Unreliable, rarely │
│                 │ retry if fails   │ used for webhooks  │
├─────────────────┼──────────────────┼────────────────────┤
│ At-least-once ← │ Send until gets  │ Stripe, PayPal,    │
│ (Most common)   │ success response │ Shopify, Square    │
│                 │ May duplicate    │ ← STANDARD         │
├─────────────────┼──────────────────┼────────────────────┤
│ Exactly-once    │ Guaranteed once  │ Expensive/complex  │
│                 │ (theoretical)    │ (Apache Kafka)     │
└─────────────────┴──────────────────┴────────────────────┘
</code></pre></div></div>

<h3 id="why-at-least-once-is-standard">Why “At-Least-Once” is Standard</h3>

<p>Imagine you’re a payment provider:</p>

<p><strong>Scenario 1: At-Most-Once</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Provider: "I'm sending webhook for $10,000 payout"
*Network timeout*
Provider: "Oh well, not retrying"
→ Merchant never knows payout succeeded ❌
</code></pre></div></div>

<p><strong>Scenario 2: At-Least-Once</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Provider: "I'm sending webhook for $10,000 payout"
*Network timeout*
Provider: "No response, retrying in 5 seconds..."
Provider: "Sending again..."
*Server responds OK*
→ Merchant gets notified (might receive duplicates) ✅
</code></pre></div></div>

<p><strong>Conclusion:</strong> At-least-once delivery is the best trade-off for reliability.</p>

<h3 id="whos-responsible-for-what">Who’s Responsible for What?</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Payment Provider's job: "Webhook will arrive at least once"
Our job: "Logic runs EXACTLY once"

→ This is called IDEMPOTENCY
</code></pre></div></div>

<h2 id="what-is-idempotency">What is Idempotency?</h2>

<h3 id="definition">Definition</h3>

<p><strong>Idempotency:</strong> Performing an operation multiple times produces the same result as performing it once.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Real-world examples:

✅ Idempotent:
   SET temperature = 25°C
   (set 10 times → still 25°C)

❌ Not idempotent:
   INCREMENT temperature +5°C
   (increment 10 times → 50°C increase!)

✅ Idempotent:
   UPDATE users SET name = 'John' WHERE id = 1

❌ Not idempotent:
   INSERT INTO payments (amount) VALUES (100)
</code></pre></div></div>

<h3 id="idempotency-in-webhooks">Idempotency in Webhooks</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Not idempotent</span>
<span class="kd">function</span> <span class="nx">chargePayoutFee</span><span class="p">(</span><span class="nx">payoutId</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nx">insert</span><span class="p">(</span><span class="dl">'</span><span class="s1">fees</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
    <span class="nx">payoutId</span><span class="p">,</span>
    <span class="na">amount</span><span class="p">:</span> <span class="mi">50</span>
  <span class="p">});</span>
  <span class="c1">// Called twice → 2 records → wrong</span>
<span class="p">}</span>

<span class="c1">// ✅ Idempotent</span>
<span class="kd">function</span> <span class="nx">chargePayoutFee</span><span class="p">(</span><span class="nx">payoutId</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">idempotencyKey</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nx">insert</span><span class="p">(</span><span class="dl">'</span><span class="s1">fees</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
    <span class="nx">payoutId</span><span class="p">,</span>
    <span class="na">amount</span><span class="p">:</span> <span class="mi">50</span><span class="p">,</span>
    <span class="nx">idempotencyKey</span> <span class="c1">// ← Unique constraint</span>
  <span class="p">});</span>
  <span class="c1">// Called twice → second fails with duplicate key → idempotent</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="the-idempotency-key">The Idempotency Key</h3>

<p>An <strong>idempotency key</strong> is a unique identifier for each webhook event.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Common implementations:

Stripe:   event.id (e.g., "evt_1ABC...")
PayPal:   event_id
Shopify:  X-Shopify-Webhook-Id header
Square:   event_id

→ Use this key to detect duplicate webhooks
</code></pre></div></div>

<h2 id="implementation-3-patterns-for-idempotency">Implementation: 3 Patterns for Idempotency</h2>

<h3 id="pattern-1-database-unique-constraint-recommended">Pattern #1: Database Unique Constraint (Recommended)</h3>

<p>This is the simplest and most reliable approach.</p>

<p><strong>Step 1: Database Schema</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">payout_fees</span> <span class="p">(</span>
  <span class="n">id</span> <span class="n">UUID</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="k">DEFAULT</span> <span class="n">gen_random_uuid</span><span class="p">(),</span>
  <span class="n">payout_id</span> <span class="n">UUID</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">amount</span> <span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">idempotency_key</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">UNIQUE</span><span class="p">,</span> <span class="c1">-- ← The magic</span>
  <span class="n">created_at</span> <span class="nb">TIMESTAMP</span> <span class="k">DEFAULT</span> <span class="n">NOW</span><span class="p">()</span>
<span class="p">);</span>

<span class="c1">-- Ensure idempotency key is unique</span>
<span class="k">CREATE</span> <span class="k">UNIQUE</span> <span class="k">INDEX</span> <span class="n">idx_payout_fees_idempotency</span>
  <span class="k">ON</span> <span class="n">payout_fees</span><span class="p">(</span><span class="n">idempotency_key</span><span class="p">);</span>

<span class="c1">-- Also ensure one payout = one fee</span>
<span class="k">CREATE</span> <span class="k">UNIQUE</span> <span class="k">INDEX</span> <span class="n">idx_payout_fees_payout_id</span>
  <span class="k">ON</span> <span class="n">payout_fees</span><span class="p">(</span><span class="n">payout_id</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>Step 2: Service Implementation</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">PayoutFeeService</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">(</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">feeRepository</span><span class="p">:</span> <span class="nx">FeeRepository</span><span class="p">,</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">logger</span><span class="p">:</span> <span class="nx">Logger</span>
  <span class="p">)</span> <span class="p">{}</span>

  <span class="k">async</span> <span class="nx">chargePayoutFee</span><span class="p">(</span>
    <span class="nx">payoutId</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
    <span class="nx">amount</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span>
    <span class="nx">idempotencyKey</span><span class="p">:</span> <span class="kr">string</span>
  <span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">PayoutFee</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span>
      <span class="c1">// Attempt to insert with idempotency key</span>
      <span class="kd">const</span> <span class="nx">fee</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">feeRepository</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
        <span class="nx">payoutId</span><span class="p">,</span>
        <span class="nx">amount</span><span class="p">,</span>
        <span class="nx">idempotencyKey</span>
      <span class="p">});</span>

      <span class="k">this</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Fee charged successfully: </span><span class="p">${</span><span class="nx">fee</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
      <span class="k">return</span> <span class="nx">fee</span><span class="p">;</span>

    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// Check if it's a duplicate key error</span>
      <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">isDuplicateKeyError</span><span class="p">(</span><span class="nx">error</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span>
          <span class="s2">`Duplicate webhook detected and ignored: </span><span class="p">${</span><span class="nx">idempotencyKey</span><span class="p">}</span><span class="s2">`</span>
        <span class="p">);</span>

        <span class="c1">// Return existing fee (idempotent response)</span>
        <span class="kd">const</span> <span class="nx">existingFee</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">feeRepository</span><span class="p">.</span><span class="nx">findOne</span><span class="p">({</span>
          <span class="nx">idempotencyKey</span>
        <span class="p">});</span>

        <span class="k">return</span> <span class="nx">existingFee</span><span class="p">;</span>
      <span class="p">}</span>

      <span class="c1">// Re-throw other errors</span>
      <span class="k">throw</span> <span class="nx">error</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="nx">isDuplicateKeyError</span><span class="p">(</span><span class="nx">error</span><span class="p">:</span> <span class="kr">any</span><span class="p">):</span> <span class="nx">boolean</span> <span class="p">{</span>
    <span class="c1">// PostgreSQL unique violation code</span>
    <span class="k">return</span> <span class="nx">error</span><span class="p">.</span><span class="nx">code</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">23505</span><span class="dl">'</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Step 3: Webhook Handler</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Controller</span><span class="p">(</span><span class="dl">'</span><span class="s1">webhooks</span><span class="dl">'</span><span class="p">)</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">WebhookController</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">(</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">webhookService</span><span class="p">:</span> <span class="nx">WebhookService</span><span class="p">,</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">payoutService</span><span class="p">:</span> <span class="nx">PayoutService</span><span class="p">,</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">feeService</span><span class="p">:</span> <span class="nx">PayoutFeeService</span><span class="p">,</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">logger</span><span class="p">:</span> <span class="nx">Logger</span>
  <span class="p">)</span> <span class="p">{}</span>

  <span class="p">@</span><span class="nd">Post</span><span class="p">(</span><span class="dl">'</span><span class="s1">payment-provider</span><span class="dl">'</span><span class="p">)</span>
  <span class="k">async</span> <span class="nx">handlePayoutWebhook</span><span class="p">(</span>
    <span class="p">@</span><span class="nd">Body</span><span class="p">()</span> <span class="nx">event</span><span class="p">:</span> <span class="nx">PayoutWebhookEvent</span><span class="p">,</span>
    <span class="p">@</span><span class="nd">Headers</span><span class="p">(</span><span class="dl">'</span><span class="s1">x-webhook-signature</span><span class="dl">'</span><span class="p">)</span> <span class="nx">signature</span><span class="p">:</span> <span class="kr">string</span>
  <span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Step 1: Verify webhook signature (security)</span>
    <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">webhookService</span><span class="p">.</span><span class="nx">verifySignature</span><span class="p">(</span><span class="nx">event</span><span class="p">,</span> <span class="nx">signature</span><span class="p">);</span>

    <span class="c1">// Step 2: Use event ID as idempotency key</span>
    <span class="kd">const</span> <span class="nx">idempotencyKey</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">;</span> <span class="c1">// e.g., "evt_123abc"</span>

    <span class="c1">// Step 3: Get payout details</span>
    <span class="kd">const</span> <span class="nx">payout</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">payoutService</span><span class="p">.</span><span class="nx">findOne</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">payoutId</span><span class="p">);</span>

    <span class="c1">// Step 4: Calculate fee</span>
    <span class="kd">const</span> <span class="nx">feeAmount</span> <span class="o">=</span> <span class="nx">payout</span><span class="p">.</span><span class="nx">amount</span> <span class="o">*</span> <span class="mf">0.005</span><span class="p">;</span> <span class="c1">// 0.5%</span>

    <span class="c1">// Step 5: Charge fee (idempotent!)</span>
    <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">feeService</span><span class="p">.</span><span class="nx">chargePayoutFee</span><span class="p">(</span>
      <span class="nx">payout</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
      <span class="nx">feeAmount</span><span class="p">,</span>
      <span class="nx">idempotencyKey</span> <span class="c1">// ← Magic happens here</span>
    <span class="p">);</span>

    <span class="k">return</span> <span class="p">{</span> <span class="na">received</span><span class="p">:</span> <span class="kc">true</span> <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Why This Works:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>First webhook arrives:
- Insert with idempotency_key = "evt_123"
- Success → Fee charged

Second webhook arrives (duplicate):
- Try to insert with idempotency_key = "evt_123"
- Fails: Unique constraint violation
- Catch error, return existing fee
- No double charge! ✅
</code></pre></div></div>

<p><strong>Pros:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ Simple to implement
✅ Database enforces uniqueness (single source of truth)
✅ Atomic operation (no race conditions)
✅ No external dependencies (Redis, etc.)
✅ Works with any SQL database
</code></pre></div></div>

<p><strong>Cons:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❌ Relies on database constraint
❌ Less flexible for complex scenarios
</code></pre></div></div>

<hr />

<h3 id="pattern-2-redis-distributed-lock">Pattern #2: Redis Distributed Lock</h3>

<blockquote>
  <p><strong>⚠️ Warning:</strong> This pattern is more complex than Pattern #1. Only use it if you have specific requirements.</p>
</blockquote>

<p><strong>When you might need this:</strong></p>
<ul>
  <li>You’re processing webhooks on multiple servers simultaneously</li>
  <li>Webhook processing takes &gt;5 seconds (long operations)</li>
  <li>You want to wait for first request to finish instead of failing fast</li>
</ul>

<p><strong>Core concept:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="nx">chargePayoutFee</span><span class="p">(</span><span class="nx">payoutId</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">amount</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">idempotencyKey</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">lockKey</span> <span class="o">=</span> <span class="s2">`lock:payout-fee:</span><span class="p">${</span><span class="nx">idempotencyKey</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>

  <span class="c1">// Try to acquire lock</span>
  <span class="kd">const</span> <span class="nx">lockAcquired</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">redis</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">lockKey</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">EX</span><span class="dl">'</span><span class="p">,</span> <span class="mi">30</span><span class="p">,</span> <span class="dl">'</span><span class="s1">NX</span><span class="dl">'</span><span class="p">);</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">lockAcquired</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Another server is processing, wait for it to finish</span>
    <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">waitForCompletion</span><span class="p">(</span><span class="nx">lockKey</span><span class="p">);</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">feeRepository</span><span class="p">.</span><span class="nx">findOne</span><span class="p">({</span> <span class="nx">idempotencyKey</span> <span class="p">});</span>
  <span class="p">}</span>

  <span class="k">try</span> <span class="p">{</span>
    <span class="c1">// We got the lock - check if already processed</span>
    <span class="kd">const</span> <span class="nx">existing</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">feeRepository</span><span class="p">.</span><span class="nx">findOne</span><span class="p">({</span> <span class="nx">idempotencyKey</span> <span class="p">});</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">existing</span><span class="p">)</span> <span class="k">return</span> <span class="nx">existing</span><span class="p">;</span>

    <span class="c1">// Process fee</span>
    <span class="k">return</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">feeRepository</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span> <span class="nx">payoutId</span><span class="p">,</span> <span class="nx">amount</span><span class="p">,</span> <span class="nx">idempotencyKey</span> <span class="p">});</span>

  <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
    <span class="c1">// Always release lock</span>
    <span class="k">await</span> <span class="nx">redis</span><span class="p">.</span><span class="nx">del</span><span class="p">(</span><span class="nx">lockKey</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Pros:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ Handles concurrent requests across multiple servers
✅ Prevents race conditions completely
✅ Second request waits instead of failing
</code></pre></div></div>

<p><strong>Cons:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❌ Significantly more complex than Pattern #1
❌ Requires Redis (external dependency)
❌ What if Redis goes down? (need fallback)
❌ Lock timeout needs careful tuning
❌ Waiting logic can be tricky
</code></pre></div></div>

<p><strong>Reality check:</strong> Pattern #1 (unique constraint) is enough for 95% of use cases. Only implement Redis locks if you have a proven need.</p>

<hr />

<h3 id="pattern-3-state-machine-with-pessimistic-locking">Pattern #3: State Machine with Pessimistic Locking</h3>

<p>For workflows with complex state transitions:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Database schema with state tracking</span>
<span class="kr">enum</span> <span class="nx">PayoutState</span> <span class="p">{</span>
  <span class="nx">PENDING</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">pending</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">FEE_CHARGED</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">fee_charged</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">COMPLETED</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">completed</span><span class="dl">'</span>
<span class="p">}</span>

<span class="c1">// Entity</span>
<span class="kd">class</span> <span class="nx">Payout</span> <span class="p">{</span>
  <span class="nl">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">amount</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="nl">status</span><span class="p">:</span> <span class="nx">PayoutState</span><span class="p">;</span>
  <span class="nl">version</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="c1">// For optimistic locking</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">PayoutFeeService</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nx">chargePayoutFee</span><span class="p">(</span><span class="nx">payoutId</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">PayoutFee</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="c1">// Use database transaction</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">entityManager</span><span class="p">.</span><span class="nx">transaction</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">em</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="c1">// Lock the payout row (SELECT FOR UPDATE)</span>
      <span class="kd">const</span> <span class="nx">payout</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">em</span><span class="p">.</span><span class="nx">findOne</span><span class="p">(</span><span class="nx">Payout</span><span class="p">,</span> <span class="nx">payoutId</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">lock</span><span class="p">:</span> <span class="p">{</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">'</span><span class="s1">pessimistic_write</span><span class="dl">'</span> <span class="p">}</span>
      <span class="p">});</span>

      <span class="c1">// Check state - only charge if pending</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">payout</span><span class="p">.</span><span class="nx">status</span> <span class="o">!==</span> <span class="nx">PayoutState</span><span class="p">.</span><span class="nx">PENDING</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Fee already charged, skipping</span><span class="dl">'</span><span class="p">);</span>
        <span class="k">return</span> <span class="nx">em</span><span class="p">.</span><span class="nx">findOne</span><span class="p">(</span><span class="nx">PayoutFee</span><span class="p">,</span> <span class="p">{</span> <span class="nx">payoutId</span> <span class="p">});</span>
      <span class="p">}</span>

      <span class="c1">// Create fee</span>
      <span class="kd">const</span> <span class="nx">fee</span> <span class="o">=</span> <span class="nx">em</span><span class="p">.</span><span class="nx">create</span><span class="p">(</span><span class="nx">PayoutFee</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">payoutId</span><span class="p">:</span> <span class="nx">payout</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
        <span class="na">amount</span><span class="p">:</span> <span class="nx">payout</span><span class="p">.</span><span class="nx">amount</span> <span class="o">*</span> <span class="mf">0.005</span>
      <span class="p">});</span>
      <span class="k">await</span> <span class="nx">em</span><span class="p">.</span><span class="nx">save</span><span class="p">(</span><span class="nx">fee</span><span class="p">);</span>

      <span class="c1">// Update state atomically</span>
      <span class="nx">payout</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="nx">PayoutState</span><span class="p">.</span><span class="nx">FEE_CHARGED</span><span class="p">;</span>
      <span class="k">await</span> <span class="nx">em</span><span class="p">.</span><span class="nx">save</span><span class="p">(</span><span class="nx">payout</span><span class="p">);</span>

      <span class="k">return</span> <span class="nx">fee</span><span class="p">;</span>
    <span class="p">});</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Pros:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ State machine is easy to reason about
✅ Transaction guarantees consistency
✅ No separate idempotency key needed
✅ Works well for complex workflows
</code></pre></div></div>

<p><strong>Cons:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❌ Tight coupling with payout entity
❌ Pessimistic lock can slow things down
❌ Harder to scale horizontally
</code></pre></div></div>

<hr />

<h3 id="comparison-table">Comparison Table</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌────────────┬─────────────┬──────────────┬─────────────┐
│ Pattern    │ Complexity  │  Performance │   Use Case  │
├────────────┼─────────────┼──────────────┼─────────────┤
│ Unique     │   Simple    │    Fast      │ ✅ Default  │
│ Constraint │   ⭐⭐       │    ⭐⭐⭐     │   choice    │
├────────────┼─────────────┼──────────────┼─────────────┤
│ Redis Lock │   Complex   │    Medium    │ High conc.  │
│            │   ⭐⭐⭐     │    ⭐⭐       │ multi-server│
├────────────┼─────────────┼──────────────┼─────────────┤
│ State      │   Medium    │    Fast      │ Complex     │
│ Machine    │   ⭐⭐⭐     │    ⭐⭐⭐     │ workflows   │
└────────────┴─────────────┴──────────────┴─────────────┘
</code></pre></div></div>

<p><strong>Recommendation:</strong> Start with Pattern #1 (Unique Constraint). It’s simple, reliable, and covers 90% of use cases.</p>

<h2 id="critical-webhook-security">Critical: Webhook Security</h2>

<p>Before processing ANY webhook, you MUST verify its signature:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Post</span><span class="p">(</span><span class="dl">'</span><span class="s1">webhooks/payment-provider</span><span class="dl">'</span><span class="p">)</span>
<span class="k">async</span> <span class="nx">handleWebhook</span><span class="p">(</span>
  <span class="p">@</span><span class="nd">Body</span><span class="p">()</span> <span class="nx">event</span><span class="p">:</span> <span class="nx">WebhookEvent</span><span class="p">,</span>
  <span class="p">@</span><span class="nd">Headers</span><span class="p">(</span><span class="dl">'</span><span class="s1">x-webhook-signature</span><span class="dl">'</span><span class="p">)</span> <span class="nx">signature</span><span class="p">:</span> <span class="kr">string</span>
<span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Step 1: ALWAYS verify signature first</span>
  <span class="kd">const</span> <span class="nx">isValid</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">verifyWebhookSignature</span><span class="p">(</span>
    <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">event</span><span class="p">),</span>
    <span class="nx">signature</span><span class="p">,</span>
    <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">WEBHOOK_SECRET</span>
  <span class="p">);</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isValid</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nx">UnauthorizedException</span><span class="p">(</span><span class="dl">'</span><span class="s1">Invalid webhook signature</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="c1">// Step 2: Process webhook (idempotent)</span>
  <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">processWebhook</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">private</span> <span class="nx">verifyWebhookSignature</span><span class="p">(</span>
  <span class="nx">payload</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
  <span class="nx">signature</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
  <span class="nx">secret</span><span class="p">:</span> <span class="kr">string</span>
<span class="p">):</span> <span class="nx">boolean</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">hmac</span> <span class="o">=</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">createHmac</span><span class="p">(</span><span class="dl">'</span><span class="s1">sha256</span><span class="dl">'</span><span class="p">,</span> <span class="nx">secret</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">digest</span> <span class="o">=</span> <span class="nx">hmac</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">payload</span><span class="p">).</span><span class="nx">digest</span><span class="p">(</span><span class="dl">'</span><span class="s1">hex</span><span class="dl">'</span><span class="p">);</span>

  <span class="k">return</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">timingSafeEqual</span><span class="p">(</span>
    <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">signature</span><span class="p">),</span>
    <span class="nx">Buffer</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">digest</span><span class="p">)</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Why this is critical:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❌ Without verification:
- Attackers can send fake webhooks
- Charge fees arbitrarily
- Manipulate balances
- Steal money

✅ With verification:
- Only legitimate provider can send webhooks
- Webhooks are cryptographically verified
- System is secure
</code></pre></div></div>

<p><strong>Never skip signature verification in production!</strong></p>

<h2 id="handling-out-of-order-webhooks">Handling Out-of-Order Webhooks</h2>

<p>Webhooks can arrive out of order:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Timeline (reality):
10:00 - Payout created (webhook #1)
10:01 - Payout processing (webhook #2)
10:02 - Payout completed (webhook #3)

Timeline (webhooks arrive):
10:00:10 - Webhook #1 arrives ✅
10:01:05 - Webhook #3 arrives ❌ (too early!)
10:01:15 - Webhook #2 arrives ❌ (delayed)
</code></pre></div></div>

<p><strong>Solution: Use timestamp or version number:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="nx">processPayoutWebhook</span><span class="p">(</span><span class="nx">event</span><span class="p">:</span> <span class="nx">PayoutWebhookEvent</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">payout</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">payoutRepo</span><span class="p">.</span><span class="nx">findOne</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">payoutId</span><span class="p">);</span>

  <span class="c1">// Ignore outdated webhooks</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">timestamp</span> <span class="o">&lt;=</span> <span class="nx">payout</span><span class="p">.</span><span class="nx">lastWebhookTimestamp</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Outdated webhook, ignoring</span><span class="dl">'</span><span class="p">);</span>
    <span class="k">return</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="c1">// Process and update timestamp</span>
  <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">updatePayoutStatus</span><span class="p">(</span><span class="nx">payout</span><span class="p">,</span> <span class="nx">event</span><span class="p">);</span>
  <span class="nx">payout</span><span class="p">.</span><span class="nx">lastWebhookTimestamp</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">timestamp</span><span class="p">;</span>
  <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">payoutRepo</span><span class="p">.</span><span class="nx">save</span><span class="p">(</span><span class="nx">payout</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="idempotency-window-how-long-to-store-keys">Idempotency Window: How Long to Store Keys?</h2>

<p><strong>Question:</strong> How long should we keep idempotency keys?</p>

<p><strong>Recommended: 24-72 hours</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Why 24-72 hours?
✅ Long enough for all webhook retry scenarios
✅ Most providers stop retrying after 24 hours
✅ Prevents database bloat from old keys
</code></pre></div></div>

<p><strong>Clean up old keys with a cron job:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Cron</span><span class="p">(</span><span class="dl">'</span><span class="s1">0 0 * * *</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// Daily at midnight</span>
<span class="k">async</span> <span class="nx">cleanupOldIdempotencyKeys</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">threeDaysAgo</span> <span class="o">=</span> <span class="nx">subDays</span><span class="p">(</span><span class="k">new</span> <span class="nb">Date</span><span class="p">(),</span> <span class="mi">3</span><span class="p">);</span>

  <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="s2">`
    DELETE FROM payout_fees
    WHERE created_at &lt; $1
  `</span><span class="p">,</span> <span class="p">[</span><span class="nx">threeDaysAgo</span><span class="p">]);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Most payment providers (Stripe, PayPal) stop retrying webhooks after 3 days, so this window is safe.</p>

<h2 id="testing-idempotency">Testing Idempotency</h2>

<h3 id="local-testing-simulate-webhooks-with-curl">Local Testing: Simulate Webhooks with curl</h3>

<p>Before writing automated tests, verify your implementation works locally:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Step 1: Start your server locally</span>
npm run dev

<span class="c"># Step 2: Send the same webhook 5 times concurrently</span>
<span class="k">for </span>i <span class="k">in</span> <span class="o">{</span>1..5<span class="o">}</span><span class="p">;</span> <span class="k">do
  </span>curl <span class="nt">-X</span> POST http://localhost:3000/webhooks/payout <span class="se">\</span>
    <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
    <span class="nt">-H</span> <span class="s2">"x-webhook-signature: test_signature"</span> <span class="se">\</span>
    <span class="nt">-d</span> <span class="s1">'{
      "id": "evt_test_duplicate",
      "payoutId": "payout_123",
      "amount": 10000,
      "status": "completed"
    }'</span> &amp;
<span class="k">done
</span><span class="nb">wait</span>

<span class="c"># Step 3: Check database - should have only 1 fee</span>
psql <span class="nt">-d</span> mydb <span class="nt">-c</span> <span class="s2">"SELECT COUNT(*) FROM payout_fees WHERE idempotency_key = 'evt_test_duplicate';"</span>
<span class="c"># Expected: 1 (not 5!)</span>
</code></pre></div></div>

<p><strong>Testing with a real payment provider:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Use ngrok to expose local server</span>
ngrok http 3000

<span class="c"># Update webhook URL in payment provider dashboard to:</span>
https://your-ngrok-url.ngrok.io/webhooks/payout

<span class="c"># Trigger test webhook from provider dashboard</span>
<span class="c"># Then check your logs and database</span>
</code></pre></div></div>

<hr />

<h3 id="unit-test-duplicate-webhooks">Unit Test: Duplicate Webhooks</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">PayoutFeeService</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">should charge fee only once for duplicate webhooks</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">payoutId</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">payout_123</span><span class="dl">'</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">idempotencyKey</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">evt_abc</span><span class="dl">'</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">amount</span> <span class="o">=</span> <span class="mi">50</span><span class="p">;</span>

    <span class="c1">// First webhook</span>
    <span class="kd">const</span> <span class="nx">fee1</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">service</span><span class="p">.</span><span class="nx">chargePayoutFee</span><span class="p">(</span>
      <span class="nx">payoutId</span><span class="p">,</span>
      <span class="nx">amount</span><span class="p">,</span>
      <span class="nx">idempotencyKey</span>
    <span class="p">);</span>

    <span class="c1">// Duplicate webhook (same idempotency key)</span>
    <span class="kd">const</span> <span class="nx">fee2</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">service</span><span class="p">.</span><span class="nx">chargePayoutFee</span><span class="p">(</span>
      <span class="nx">payoutId</span><span class="p">,</span>
      <span class="nx">amount</span><span class="p">,</span>
      <span class="nx">idempotencyKey</span>
    <span class="p">);</span>

    <span class="c1">// Should return same fee object</span>
    <span class="nx">expect</span><span class="p">(</span><span class="nx">fee1</span><span class="p">.</span><span class="nx">id</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">fee2</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>

    <span class="c1">// Should only have 1 record in database</span>
    <span class="kd">const</span> <span class="nx">count</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">feeRepo</span><span class="p">.</span><span class="nx">count</span><span class="p">({</span> <span class="nx">payoutId</span> <span class="p">});</span>
    <span class="nx">expect</span><span class="p">(</span><span class="nx">count</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="integration-test-concurrent-webhooks">Integration Test: Concurrent Webhooks</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">PayoutWebhookController (Integration)</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">should handle 10 concurrent duplicate webhooks</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">event</span> <span class="o">=</span> <span class="p">{</span>
      <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">evt_duplicate_test</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">payoutId</span><span class="p">:</span> <span class="dl">'</span><span class="s1">payout_123</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">amount</span><span class="p">:</span> <span class="mi">10000</span><span class="p">,</span>
      <span class="na">status</span><span class="p">:</span> <span class="dl">'</span><span class="s1">completed</span><span class="dl">'</span>
    <span class="p">};</span>

    <span class="c1">// Send 10 webhooks concurrently</span>
    <span class="kd">const</span> <span class="nx">promises</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">(</span><span class="mi">10</span><span class="p">).</span><span class="nx">fill</span><span class="p">(</span><span class="kc">null</span><span class="p">).</span><span class="nx">map</span><span class="p">(()</span> <span class="o">=&gt;</span>
      <span class="nx">request</span><span class="p">(</span><span class="nx">app</span><span class="p">.</span><span class="nx">getHttpServer</span><span class="p">())</span>
        <span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/webhooks/payout</span><span class="dl">'</span><span class="p">)</span>
        <span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">x-webhook-signature</span><span class="dl">'</span><span class="p">,</span> <span class="nx">generateSignature</span><span class="p">(</span><span class="nx">event</span><span class="p">))</span>
        <span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span>
        <span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="mi">200</span><span class="p">)</span>
    <span class="p">);</span>

    <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span><span class="nx">promises</span><span class="p">);</span>

    <span class="c1">// Should only create 1 fee</span>
    <span class="kd">const</span> <span class="nx">fees</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">feeRepo</span><span class="p">.</span><span class="nx">find</span><span class="p">({</span>
      <span class="na">payoutId</span><span class="p">:</span> <span class="dl">'</span><span class="s1">payout_123</span><span class="dl">'</span>
    <span class="p">});</span>
    <span class="nx">expect</span><span class="p">(</span><span class="nx">fees</span><span class="p">).</span><span class="nx">toHaveLength</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
    <span class="nx">expect</span><span class="p">(</span><span class="nx">fees</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">amount</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">50</span><span class="p">);</span> <span class="c1">// 0.5% of 10000</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="manual-testing">Manual Testing</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Test duplicate webhooks manually</span>
<span class="k">for </span>i <span class="k">in</span> <span class="o">{</span>1..5<span class="o">}</span><span class="p">;</span> <span class="k">do
  </span>curl <span class="nt">-X</span> POST http://localhost:3000/webhooks/payout <span class="se">\</span>
    <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
    <span class="nt">-H</span> <span class="s2">"x-webhook-signature: &lt;signature&gt;"</span> <span class="se">\</span>
    <span class="nt">-d</span> <span class="s1">'{
      "id": "evt_test_123",
      "payoutId": "payout_xyz",
      "amount": 10000,
      "status": "completed"
    }'</span> &amp;
<span class="k">done
</span><span class="nb">wait</span>

<span class="c"># Check database</span>
psql <span class="nt">-d</span> mydb <span class="nt">-c</span> <span class="s2">"
  SELECT COUNT(*) FROM payout_fees
  WHERE payout_id = 'payout_xyz';
"</span>
<span class="c"># Expected: 1</span>
</code></pre></div></div>

<h2 id="best-practices-checklist">Best Practices Checklist</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Webhook Idempotency Checklist:

✅ Verify webhook signature BEFORE processing
✅ Use idempotency key (event.id) with unique constraint
✅ Handle duplicate key errors gracefully
✅ Return 200 OK even for duplicate webhooks
✅ Log duplicate webhooks for monitoring
✅ Handle out-of-order webhooks (timestamp check)
✅ Test with concurrent duplicate webhooks
✅ Clean up old idempotency keys (24-72h window)
✅ Use transactions for multi-step operations
✅ Monitor duplicate webhook rate

Security Checklist:

✅ Verify HMAC signature on every webhook
✅ Use timing-safe comparison (crypto.timingSafeEqual)
✅ Store webhook secret in environment variables
✅ Rotate webhook secrets periodically
✅ Rate limit webhook endpoints
✅ Log failed signature verifications (potential attacks)
</code></pre></div></div>

<h2 id="the-results">The Results</h2>

<p>After implementing idempotency with Pattern #1:</p>

<p><strong>✅ Zero duplicate charges</strong> since deployment (6 months and counting)</p>

<p><strong>✅ Handled 50,000+ webhooks</strong> without issues</p>

<p><strong>✅ Processed 1,247 duplicate webhooks</strong> correctly (2.5% duplicate rate)</p>

<p><strong>✅ No production incidents</strong> related to webhook processing</p>

<p><strong>Monitoring metrics:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- Webhook success rate: 99.97%
- Duplicate webhook rate: 2.5% (normal)
- Average processing time: 120ms
- Failed signature verifications: 3 (potential attacks, blocked)
</code></pre></div></div>

<h2 id="key-takeaways">Key Takeaways</h2>

<p>1️⃣ <strong>Payment providers send webhooks “at-least-once”</strong> → Prepare for duplicates</p>

<p>2️⃣ <strong>Idempotency key = event.id</strong> → Use it with unique constraint</p>

<p>3️⃣ <strong>Database unique constraint is simple &amp; effective</strong> → Pattern #1 covers 90% of cases</p>

<p>4️⃣ <strong>ALWAYS verify webhook signatures</strong> → Security is not optional</p>

<p>5️⃣ <strong>Test with concurrent requests</strong> → Race conditions are real</p>

<p>6️⃣ <strong>Clean up old keys</strong> → 24-72 hour window is enough</p>

<p>7️⃣ <strong>Log duplicate webhooks</strong> → Monitor for abnormal patterns</p>

<p>8️⃣ <strong>Return idempotent responses</strong> → Same input = same output</p>

<p><strong>Remember:</strong> “Webhooks being sent multiple times is normal. Your system processing them exactly once is your responsibility.”</p>

<hr />

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://stripe.com/docs/webhooks/best-practices">Stripe Webhook Best Practices</a></li>
  <li><a href="https://datatracker.ietf.org/doc/html/draft-idempotency-header">Idempotency Keys - RFC Draft</a></li>
  <li><a href="https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/">AWS Builders Library: Making Retries Safe with Idempotent APIs</a></li>
</ul>

<p><em>Have you dealt with webhook idempotency issues? What patterns have worked for you? Share your experience in the comments!</em></p>]]></content><author><name>Thanh Nguyen</name></author><category term="Backend" /><category term="Payment Systems" /><category term="webhook" /><category term="idempotency" /><category term="payment" /><category term="race-condition" /><category term="distributed-systems" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Shared PDF Generation Between Frontend and Backend: One Template, Two Platforms</title><link href="https://beautyoncode.com/backend/full-stack/shared-pdf-generation-frontend-backend-monorepo/" rel="alternate" type="text/html" title="Shared PDF Generation Between Frontend and Backend: One Template, Two Platforms" /><published>2025-07-10T00:00:00-04:00</published><updated>2025-07-10T00:00:00-04:00</updated><id>https://beautyoncode.com/backend/full-stack/shared-pdf-generation-frontend-backend-monorepo</id><content type="html" xml:base="https://beautyoncode.com/backend/full-stack/shared-pdf-generation-frontend-backend-monorepo/"><![CDATA[<p><img src="/assets/images/2026/05/2026-05-19-shared-pdf-generation-cover.webp" alt="" /></p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#the-problem-pdf-inconsistency-nightmare">The Problem</a></li>
  <li><a href="#solution-shared-package-with-react-pdf">Solution: Shared Package</a></li>
  <li><a href="#implementation-building-the-pdf-package">Implementation</a></li>
  <li><a href="#using-the-package">Usage (Frontend &amp; Backend)</a></li>
  <li><a href="#best-practices--takeaways">Best Practices</a></li>
</ul>

<hr />

<h2 id="the-problem-pdf-inconsistency-nightmare">The Problem: PDF Inconsistency Nightmare</h2>

<p><em>“Hey, the PDF downloaded from the website is different from the one in the email…”</em></p>

<p><em>“The customer name in the web download shows ‘John Doe’, but the email attachment shows ‘Doe Electronics Inc.’ - which one is correct?”</em></p>

<p>That message from our QA team made me realize: maintaining two separate PDF codebases (frontend + backend) is a recipe for disaster.</p>

<p>Today I’m sharing how we built a shared PDF package that works on both browser (Next.js) and server (NestJS), ensuring:</p>
<ul>
  <li>✅ PDFs from both sources are identical</li>
  <li>✅ No code duplication</li>
  <li>✅ Type-safe with TypeScript</li>
  <li>✅ Easy to maintain and extend</li>
</ul>

<p>Main tools: <strong>@react-pdf/renderer</strong> + <strong>Turborepo monorepo</strong></p>

<h2 id="why-we-need-pdf-in-multiple-places">Why We Need PDF in Multiple Places</h2>

<p>In an e-commerce system, we need to generate PDFs in three different scenarios:</p>

<p><strong>1️⃣ Frontend (Browser):</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User clicks "Download Invoice"
→ Generate PDF in browser
→ No API call needed
→ Instant download
</code></pre></div></div>

<p><strong>2️⃣ Backend (Email Attachment):</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Order confirmed → Send email with PDF invoice
→ Generate PDF on server
→ Attach to email
</code></pre></div></div>

<p><strong>3️⃣ Backend (API Endpoint):</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /api/invoices/:id/pdf
→ Generate PDF on-demand
→ Stream to browser
</code></pre></div></div>

<h2 id="the-naive-approach-and-why-it-fails">The Naive Approach (And Why It Fails)</h2>

<p>Here’s what many teams do initially:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Approach: Duplicate code</span>

<span class="c1">// Frontend (using jsPDF)</span>
<span class="kd">function</span> <span class="nx">generateInvoicePDF</span><span class="p">(</span><span class="nx">invoice</span><span class="p">:</span> <span class="nx">Invoice</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">doc</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">jsPDF</span><span class="p">();</span>
  <span class="nx">doc</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="s2">`Invoice #</span><span class="p">${</span><span class="nx">invoice</span><span class="p">.</span><span class="kr">number</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
  <span class="nx">doc</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="s2">`Customer: </span><span class="p">${</span><span class="nx">invoice</span><span class="p">.</span><span class="nx">customer</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">);</span>
  <span class="nx">doc</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="s2">`Total: $</span><span class="p">${</span><span class="nx">invoice</span><span class="p">.</span><span class="nx">total</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">30</span><span class="p">);</span>
  <span class="c1">// ... 100 lines of code</span>
<span class="p">}</span>

<span class="c1">// Backend (using PDFKit)</span>
<span class="kd">function</span> <span class="nx">generateInvoicePDF</span><span class="p">(</span><span class="nx">invoice</span><span class="p">:</span> <span class="nx">Invoice</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">doc</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PDFDocument</span><span class="p">();</span>
  <span class="nx">doc</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="s2">`Invoice #</span><span class="p">${</span><span class="nx">invoice</span><span class="p">.</span><span class="kr">number</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
  <span class="nx">doc</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="s2">`Customer: </span><span class="p">${</span><span class="nx">invoice</span><span class="p">.</span><span class="nx">customer</span><span class="p">.</span><span class="nx">businessName</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">);</span> <span class="c1">// ← Different!</span>
  <span class="nx">doc</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="s2">`Total: $</span><span class="p">${</span><span class="nx">invoice</span><span class="p">.</span><span class="nx">total</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">30</span><span class="p">);</span>
  <span class="c1">// ... 100 lines of DIFFERENT code</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Problems with this approach:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❌ Code duplication → maintain two places
❌ Different logic → different outputs
❌ Inconsistent data mapping → bugs
❌ Different styling → poor UX
❌ Update one place, forget the other → disaster
</code></pre></div></div>

<p><strong>Real bugs we encountered:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Bug #1: Bill-to field
- Browser PDF: Shows customer.name ✅
- Email PDF: Shows customer.businessName ❌
→ Customer complained "the name is wrong"

Bug #2: Product pricing
- Browser: Shows discounted unit price
- Email: Shows original price
→ Numbers don't match

Bug #3: Logo rendering
- Browser: Logo looks crisp
- Email: Logo stretched/pixelated
→ Unprofessional look
</code></pre></div></div>

<h2 id="solution-shared-package-with-react-pdf">Solution: Shared Package with @react-pdf</h2>

<h3 id="why-react-pdfrenderer">Why @react-pdf/renderer?</h3>

<p>Let’s compare popular PDF libraries:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────────┬────────┬────────┬─────────────┬──────────┐
│   Library    │Browser │ Server │   Approach  │  Rating  │
├──────────────┼────────┼────────┼─────────────┼──────────┤
│ jsPDF        │   ✅   │   ❌   │ Imperative  │   ⭐⭐   │
│ PDFKit       │   ❌   │   ✅   │ Imperative  │   ⭐⭐   │
│ Puppeteer    │   ❌   │   ✅   │ HTML to PDF │  ⭐⭐⭐  │
│ @react-pdf   │   ✅   │   ✅   │ Declarative │ ⭐⭐⭐⭐ │← Pick this
└──────────────┴────────┴────────┴─────────────┴──────────┘
</code></pre></div></div>

<p><strong>Why @react-pdf wins:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ Works on both browser &amp; Node.js
✅ React component-based (familiar to most developers)
✅ Type-safe with TypeScript
✅ Reusable components
✅ Great documentation &amp; active community
✅ Easy styling with React-style objects
</code></pre></div></div>

<h3 id="architecture-overview">Architecture Overview</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ecommerce-monorepo/
├── packages/
│   └── pdf-templates/              # ← Shared package
│       ├── src/
│       │   ├── invoice/
│       │   │   ├── InvoiceTemplate.tsx
│       │   │   ├── InvoiceHeader.tsx
│       │   │   └── styles.ts
│       │   ├── components/
│       │   │   ├── Button.tsx
│       │   │   ├── Table.tsx
│       │   │   └── Badge.tsx
│       │   ├── utils/
│       │   │   └── data-transformer.ts
│       │   └── index.ts
│       └── package.json
│
├── apps/
│   ├── web/                        # Next.js frontend
│   │   └── uses pdf-templates ✅
│   │
│   └── api/                        # NestJS backend
│       └── uses pdf-templates ✅
│
└── turbo.json                      # Turborepo config
</code></pre></div></div>

<h2 id="implementation-building-the-pdf-package">Implementation: Building the PDF Package</h2>

<h3 id="step-1-package-setup">Step 1: Package Setup</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">packages/pdf-templates/package.json</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@my-shop/pdf-templates"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"main"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.js"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"types"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.d.ts"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsc"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsc --watch"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"peerDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"react"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^18.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@react-pdf/renderer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^3.1.0"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"typescript"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^5.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@types/react"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^18.0.0"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="step-2-typescript-interfaces-type-safety-first">Step 2: TypeScript Interfaces (Type Safety First!)</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// packages/pdf-templates/src/types.ts</span>

<span class="k">export</span> <span class="kr">interface</span> <span class="nx">InvoicePDFProps</span> <span class="p">{</span>
  <span class="c1">// Basic info</span>
  <span class="nl">invoiceNumber</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">invoiceDate</span><span class="p">:</span> <span class="nb">Date</span><span class="p">;</span>
  <span class="nl">dueDate</span><span class="p">:</span> <span class="nb">Date</span><span class="p">;</span>

  <span class="c1">// Merchant info</span>
  <span class="nl">merchant</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="nl">logo</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>        <span class="c1">// URL or base64</span>
    <span class="nl">address</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="nl">phone</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="p">};</span>

  <span class="c1">// Customer info (single source of truth!)</span>
  <span class="nl">billTo</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>         <span class="c1">// ← Consistent field name</span>
    <span class="nl">address</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="nl">email</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="p">};</span>

  <span class="c1">// Line items</span>
  <span class="nl">lineItems</span><span class="p">:</span> <span class="nb">Array</span><span class="o">&lt;</span><span class="p">{</span>
    <span class="na">description</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
    <span class="nl">quantity</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
    <span class="nl">unitPrice</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
    <span class="nl">total</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="p">}</span><span class="o">&gt;</span><span class="p">;</span>

  <span class="c1">// Totals</span>
  <span class="nl">subtotal</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="nl">tax</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="nl">total</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>

  <span class="c1">// Optional fields</span>
  <span class="nl">paymentTerms</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">notes</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Why this matters:</strong> Having a clear interface prevents mapping errors and makes it impossible to pass wrong data types.</p>

<h3 id="step-3-building-reusable-components">Step 3: Building Reusable Components</h3>

<p>Build a component library for common PDF elements:</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/components/InvoiceHeader.tsx</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">View</span><span class="p">,</span> <span class="nx">Text</span><span class="p">,</span> <span class="nx">Image</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-pdf/renderer</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">InvoiceHeader</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">merchantName</span><span class="p">,</span> <span class="nx">merchantLogo</span><span class="p">,</span> <span class="nx">invoiceNumber</span><span class="p">,</span> <span class="nx">invoiceDate</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">(</span>
  <span class="p">&lt;</span><span class="nc">View</span> <span class="na">style</span><span class="p">=&gt;</span>
    <span class="si">{</span><span class="nx">merchantLogo</span> <span class="o">&amp;&amp;</span> <span class="p">&lt;</span><span class="nc">Image</span> <span class="na">src</span><span class="p">=</span><span class="si">{</span><span class="nx">merchantLogo</span><span class="si">}</span> <span class="na">style</span><span class="p">=</span> <span class="p">/&gt;</span><span class="si">}</span>
    <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span><span class="si">{</span><span class="nx">merchantName</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nc">View</span> <span class="na">style</span><span class="p">=&gt;</span>
      <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span>INVOICE<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span>#<span class="si">{</span><span class="nx">invoiceNumber</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span><span class="si">{</span><span class="nx">invoiceDate</span><span class="p">.</span><span class="nx">toLocaleDateString</span><span class="p">()</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>
  <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>
<span class="p">);</span>
</code></pre></div></div>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/components/LineItemsTable.tsx</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">View</span><span class="p">,</span> <span class="nx">Text</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-pdf/renderer</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">LineItemsTable</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">items</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">(</span>
  <span class="p">&lt;</span><span class="nc">View</span> <span class="na">style</span><span class="p">=&gt;</span>
    <span class="si">{</span><span class="cm">/* Header */</span><span class="si">}</span>
    <span class="p">&lt;</span><span class="nc">View</span> <span class="na">style</span><span class="p">=&gt;</span>
      <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span>Description<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span>Qty<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span>Price<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span>Total<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>

    <span class="si">{</span><span class="cm">/* Rows */</span><span class="si">}</span>
    <span class="si">{</span><span class="nx">items</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">item</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span>
      <span class="p">&lt;</span><span class="nc">View</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">i</span><span class="si">}</span> <span class="na">style</span><span class="p">=&gt;</span>
        <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span><span class="si">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">description</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span><span class="si">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">quantity</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span>$<span class="si">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">unitPrice</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span>$<span class="si">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">total</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
      <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>
    <span class="p">))</span><span class="si">}</span>
  <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>
<span class="p">);</span>
</code></pre></div></div>

<blockquote>
  <p><strong>💡 Tip:</strong> Keep styles inline for simple components. For complex styling, use <code class="language-plaintext highlighter-rouge">StyleSheet.create()</code>.</p>
</blockquote>

<h3 id="step-4-main-invoice-template">Step 4: Main Invoice Template</h3>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/invoice/InvoiceTemplate.tsx</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Document</span><span class="p">,</span> <span class="nx">Page</span><span class="p">,</span> <span class="nx">View</span><span class="p">,</span> <span class="nx">Text</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-pdf/renderer</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">InvoiceHeader</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../components/InvoiceHeader</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LineItemsTable</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../components/LineItemsTable</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">InvoiceTemplate</span> <span class="o">=</span> <span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span>
  <span class="p">&lt;</span><span class="nc">Document</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nc">Page</span> <span class="na">size</span><span class="p">=</span><span class="s">"A4"</span> <span class="na">style</span><span class="p">=&gt;</span>

      <span class="si">{</span><span class="cm">/* Header */</span><span class="si">}</span>
      <span class="p">&lt;</span><span class="nc">InvoiceHeader</span>
        <span class="na">merchantName</span><span class="p">=</span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">merchant</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span>
        <span class="na">merchantLogo</span><span class="p">=</span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">merchant</span><span class="p">.</span><span class="nx">logo</span><span class="si">}</span>
        <span class="na">invoiceNumber</span><span class="p">=</span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">invoiceNumber</span><span class="si">}</span>
        <span class="na">invoiceDate</span><span class="p">=</span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">invoiceDate</span><span class="si">}</span>
      <span class="p">/&gt;</span>

      <span class="si">{</span><span class="cm">/* Merchant &amp; Customer */</span><span class="si">}</span>
      <span class="p">&lt;</span><span class="nc">View</span> <span class="na">style</span><span class="p">=&gt;</span>
        <span class="p">&lt;</span><span class="nc">View</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span>FROM:<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">merchant</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">merchant</span><span class="p">.</span><span class="nx">address</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">View</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span>BILL TO:<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">billTo</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">billTo</span><span class="p">.</span><span class="nx">address</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>
      <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>

      <span class="si">{</span><span class="cm">/* Line Items */</span><span class="si">}</span>
      <span class="p">&lt;</span><span class="nc">LineItemsTable</span> <span class="na">items</span><span class="p">=</span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">lineItems</span><span class="si">}</span> <span class="p">/&gt;</span>

      <span class="si">{</span><span class="cm">/* Totals */</span><span class="si">}</span>
      <span class="p">&lt;</span><span class="nc">View</span> <span class="na">style</span><span class="p">=&gt;</span>
        <span class="p">&lt;</span><span class="nc">View</span> <span class="na">style</span><span class="p">=&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span><span class="p">&gt;</span>Subtotal:<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span><span class="p">&gt;</span>$<span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">subtotal</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">View</span> <span class="na">style</span><span class="p">=&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span><span class="p">&gt;</span>Tax:<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span><span class="p">&gt;</span>$<span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">tax</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">View</span> <span class="na">style</span><span class="p">=&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span><span class="p">&gt;</span>Total:<span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
          <span class="p">&lt;</span><span class="nc">Text</span><span class="p">&gt;</span>$<span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">total</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>
      <span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>

      <span class="si">{</span><span class="cm">/* Footer */</span><span class="si">}</span>
      <span class="p">&lt;</span><span class="nc">Text</span> <span class="na">style</span><span class="p">=&gt;</span>
        Thank you for your business!
      <span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nc">Page</span><span class="p">&gt;</span>
  <span class="p">&lt;/</span><span class="nc">Document</span><span class="p">&gt;</span>
<span class="p">);</span>
</code></pre></div></div>

<blockquote>
  <p><strong>📖 Full Example:</strong> See <a href="https://react-pdf.org/">@react-pdf documentation</a> for advanced styling and components.</p>
</blockquote>

<h3 id="step-5-data-transformation-layer-critical">Step 5: Data Transformation Layer (Critical!)</h3>

<p><strong>This is the key to consistency.</strong> Create ONE function to transform your Order entity into PDF props:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/utils/data-transformer.ts</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">toPDFProps</span><span class="p">(</span><span class="nx">order</span><span class="p">:</span> <span class="nx">Order</span><span class="p">):</span> <span class="nx">InvoicePDFProps</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="na">invoiceNumber</span><span class="p">:</span> <span class="nx">order</span><span class="p">.</span><span class="nx">invoiceNumber</span><span class="p">,</span>
    <span class="na">invoiceDate</span><span class="p">:</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">order</span><span class="p">.</span><span class="nx">createdAt</span><span class="p">),</span>
    <span class="na">dueDate</span><span class="p">:</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">order</span><span class="p">.</span><span class="nx">dueDate</span><span class="p">),</span>

    <span class="na">merchant</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="nx">order</span><span class="p">.</span><span class="nx">merchant</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
      <span class="na">logo</span><span class="p">:</span> <span class="nx">order</span><span class="p">.</span><span class="nx">merchant</span><span class="p">.</span><span class="nx">logoUrl</span> <span class="p">?</span> <span class="s2">`https://cdn.myshop.com/</span><span class="p">${</span><span class="nx">order</span><span class="p">.</span><span class="nx">merchant</span><span class="p">.</span><span class="nx">logoUrl</span><span class="p">}</span><span class="s2">`</span> <span class="p">:</span> <span class="kc">undefined</span><span class="p">,</span>
      <span class="na">address</span><span class="p">:</span> <span class="nx">formatAddress</span><span class="p">(</span><span class="nx">order</span><span class="p">.</span><span class="nx">merchant</span><span class="p">.</span><span class="nx">address</span><span class="p">)</span>
    <span class="p">},</span>

    <span class="c1">// ✅ Single source of truth - ALWAYS use customer.fullName</span>
    <span class="na">billTo</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="nx">order</span><span class="p">.</span><span class="nx">customer</span><span class="p">.</span><span class="nx">fullName</span><span class="p">,</span>  <span class="c1">// Not businessName!</span>
      <span class="na">address</span><span class="p">:</span> <span class="nx">formatAddress</span><span class="p">(</span><span class="nx">order</span><span class="p">.</span><span class="nx">customer</span><span class="p">.</span><span class="nx">billingAddress</span><span class="p">),</span>
      <span class="na">email</span><span class="p">:</span> <span class="nx">order</span><span class="p">.</span><span class="nx">customer</span><span class="p">.</span><span class="nx">email</span>
    <span class="p">},</span>

    <span class="na">lineItems</span><span class="p">:</span> <span class="nx">order</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">item</span> <span class="o">=&gt;</span> <span class="p">({</span>
      <span class="na">description</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">product</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
      <span class="na">quantity</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">quantity</span><span class="p">,</span>
      <span class="na">unitPrice</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">unitPrice</span><span class="p">,</span>
      <span class="na">total</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">quantity</span> <span class="o">*</span> <span class="nx">item</span><span class="p">.</span><span class="nx">unitPrice</span>
    <span class="p">})),</span>

    <span class="na">subtotal</span><span class="p">:</span> <span class="nx">order</span><span class="p">.</span><span class="nx">subtotal</span><span class="p">,</span>
    <span class="na">tax</span><span class="p">:</span> <span class="nx">order</span><span class="p">.</span><span class="nx">tax</span><span class="p">,</span>
    <span class="na">total</span><span class="p">:</span> <span class="nx">order</span><span class="p">.</span><span class="nx">total</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Why this matters:</strong></p>
<ul>
  <li>✅ Used by BOTH frontend and backend</li>
  <li>✅ Impossible to have different mapping logic</li>
  <li>✅ Fix bug once, fixed everywhere</li>
  <li>✅ Type-safe with TypeScript</li>
</ul>

<h3 id="step-6-export-the-package">Step 6: Export the Package</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/index.ts</span>
<span class="k">export</span> <span class="p">{</span> <span class="nx">InvoiceTemplate</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./invoice/InvoiceTemplate</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="p">{</span> <span class="nx">toPDFProps</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./utils/data-transformer</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">type</span> <span class="p">{</span> <span class="nx">InvoicePDFProps</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./types</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="using-the-package">Using the Package</h2>

<h3 id="frontend-browser-download">Frontend (Browser Download)</h3>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Button component in Next.js/React</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">pdf</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-pdf/renderer</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">InvoiceTemplate</span><span class="p">,</span> <span class="nx">toPDFProps</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@my-shop/pdf-templates</span><span class="dl">'</span><span class="p">;</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">handleDownload</span><span class="p">(</span><span class="nx">order</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// 1. Transform data</span>
  <span class="kd">const</span> <span class="nx">pdfProps</span> <span class="o">=</span> <span class="nx">toPDFProps</span><span class="p">(</span><span class="nx">order</span><span class="p">);</span>

  <span class="c1">// 2. Generate PDF blob</span>
  <span class="kd">const</span> <span class="nx">blob</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">pdf</span><span class="p">(&lt;</span><span class="nc">InvoiceTemplate</span> <span class="si">{</span><span class="p">...</span><span class="nx">pdfProps</span><span class="si">}</span> <span class="p">/&gt;).</span><span class="nx">toBlob</span><span class="p">();</span>

  <span class="c1">// 3. Trigger download</span>
  <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">URL</span><span class="p">.</span><span class="nx">createObjectURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">link</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">);</span>
  <span class="nx">link</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
  <span class="nx">link</span><span class="p">.</span><span class="nx">download</span> <span class="o">=</span> <span class="s2">`invoice-</span><span class="p">${</span><span class="nx">order</span><span class="p">.</span><span class="nx">invoiceNumber</span><span class="p">}</span><span class="s2">.pdf`</span><span class="p">;</span>
  <span class="nx">link</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span>
  <span class="nx">URL</span><span class="p">.</span><span class="nx">revokeObjectURL</span><span class="p">(</span><span class="nx">url</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="backend-email-attachment--api">Backend (Email Attachment / API)</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// NestJS PDF service</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">renderToStream</span><span class="p">,</span> <span class="nx">renderToBuffer</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-pdf/renderer</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">InvoiceTemplate</span><span class="p">,</span> <span class="nx">toPDFProps</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@my-shop/pdf-templates</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// For email attachments</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">generatePDFForEmail</span><span class="p">(</span><span class="nx">order</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">pdfProps</span> <span class="o">=</span> <span class="nx">toPDFProps</span><span class="p">(</span><span class="nx">order</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">pdfBuffer</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">renderToBuffer</span><span class="p">(</span><span class="o">&lt;</span><span class="nx">InvoiceTemplate</span> <span class="p">{...</span><span class="nx">pdfProps</span><span class="p">}</span> <span class="sr">/&gt;</span><span class="se">)</span><span class="err">;
</span>
  <span class="k">await</span> <span class="nx">mailer</span><span class="p">.</span><span class="nx">send</span><span class="p">({</span>
    <span class="na">to</span><span class="p">:</span> <span class="nx">order</span><span class="p">.</span><span class="nx">customer</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span>
    <span class="na">subject</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Your Invoice</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">attachments</span><span class="p">:</span> <span class="p">[{</span>
      <span class="na">filename</span><span class="p">:</span> <span class="s2">`invoice-</span><span class="p">${</span><span class="nx">order</span><span class="p">.</span><span class="nx">invoiceNumber</span><span class="p">}</span><span class="s2">.pdf`</span><span class="p">,</span>
      <span class="na">content</span><span class="p">:</span> <span class="nx">pdfBuffer</span>
    <span class="p">}]</span>
  <span class="p">});</span>
<span class="p">}</span>

<span class="c1">// For API endpoint (GET /orders/:id/pdf)</span>
<span class="p">@</span><span class="nd">Get</span><span class="p">(</span><span class="dl">'</span><span class="s1">:id/pdf</span><span class="dl">'</span><span class="p">)</span>
<span class="k">async</span> <span class="nx">downloadPDF</span><span class="p">(@</span><span class="nd">Param</span><span class="p">(</span><span class="dl">'</span><span class="s1">id</span><span class="dl">'</span><span class="p">)</span> <span class="nx">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="p">@</span><span class="nd">Res</span><span class="p">()</span> <span class="nx">res</span><span class="p">:</span> <span class="nx">Response</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">order</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">ordersService</span><span class="p">.</span><span class="nx">findOne</span><span class="p">(</span><span class="nx">id</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">pdfProps</span> <span class="o">=</span> <span class="nx">toPDFProps</span><span class="p">(</span><span class="nx">order</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">stream</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">renderToStream</span><span class="p">(</span><span class="o">&lt;</span><span class="nx">InvoiceTemplate</span> <span class="p">{...</span><span class="nx">pdfProps</span><span class="p">}</span> <span class="sr">/&gt;</span><span class="se">)</span><span class="err">;
</span>
  <span class="nx">res</span><span class="p">.</span><span class="kd">set</span><span class="p">({</span>
    <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/pdf</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">Content-Disposition</span><span class="dl">'</span><span class="p">:</span> <span class="s2">`attachment; filename="invoice-</span><span class="p">${</span><span class="nx">order</span><span class="p">.</span><span class="nx">invoiceNumber</span><span class="p">}</span><span class="s2">.pdf"`</span>
  <span class="p">});</span>

  <span class="k">return</span> <span class="k">new</span> <span class="nx">StreamableFile</span><span class="p">(</span><span class="nx">stream</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Key points:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">renderToBuffer()</code> for email attachments</li>
  <li><code class="language-plaintext highlighter-rouge">renderToStream()</code> for API streaming</li>
  <li>Same <code class="language-plaintext highlighter-rouge">toPDFProps()</code> transformation used everywhere</li>
</ul>

<h2 id="common-gotchas">Common Gotchas</h2>

<h3 id="1-images-must-use-absolute-urls">1. Images Must Use Absolute URLs</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Relative paths don't work</span>
<span class="o">&lt;</span><span class="nx">Image</span> <span class="nx">src</span><span class="o">=</span><span class="dl">"</span><span class="s2">/assets/logo.png</span><span class="dl">"</span> <span class="o">/&gt;</span>

<span class="c1">// ✅ Use absolute URLs or base64</span>
<span class="o">&lt;</span><span class="nx">Image</span> <span class="nx">src</span><span class="o">=</span><span class="dl">"</span><span class="s2">https://cdn.myshop.com/logo.png</span><span class="dl">"</span> <span class="o">/&gt;</span>
<span class="o">&lt;</span><span class="nx">Image</span> <span class="nx">src</span><span class="o">=</span><span class="dl">"</span><span class="s2">data:image/png;base64,iVBORw0KG...</span><span class="dl">"</span> <span class="o">/&gt;</span>
</code></pre></div></div>

<h3 id="2-lazy-load-on-frontend-bundle-size">2. Lazy Load on Frontend (Bundle Size)</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Lazy load to avoid 200KB in initial bundle</span>
<span class="kd">const</span> <span class="nx">handleDownload</span> <span class="o">=</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">pdf</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">@react-pdf/renderer</span><span class="dl">'</span><span class="p">);</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">InvoiceTemplate</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">@my-shop/pdf-templates</span><span class="dl">'</span><span class="p">);</span>
  <span class="c1">// Generate PDF...</span>
<span class="p">};</span>
</code></pre></div></div>

<h3 id="3-custom-fonts">3. Custom Fonts</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Font</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-pdf/renderer</span><span class="dl">'</span><span class="p">;</span>

<span class="nx">Font</span><span class="p">.</span><span class="nx">register</span><span class="p">({</span>
  <span class="na">family</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Roboto</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">src</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxP.ttf</span><span class="dl">'</span>
<span class="p">});</span>

<span class="c1">// Then use in styles</span>
<span class="o">&lt;</span><span class="nx">Text</span> <span class="nx">style</span><span class="o">=&gt;</span><span class="nx">Hello</span><span class="o">&lt;</span><span class="sr">/Text</span><span class="err">&gt;
</span></code></pre></div></div>

<h2 id="testing">Testing</h2>

<h3 id="unit-test-pdf-generates-successfully">Unit Test: PDF Generates Successfully</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">pdf</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-pdf/renderer</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">InvoiceTemplate</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../src</span><span class="dl">'</span><span class="p">;</span>

<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">should generate PDF without errors</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">mockProps</span> <span class="o">=</span> <span class="p">{</span> <span class="cm">/* minimal props */</span> <span class="p">};</span>

  <span class="kd">const</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">pdf</span><span class="p">(</span><span class="o">&lt;</span><span class="nx">InvoiceTemplate</span> <span class="p">{...</span><span class="nx">mockProps</span><span class="p">}</span> <span class="sr">/&gt;</span><span class="se">)</span><span class="sr">.toBuffer</span><span class="se">()</span><span class="err">;
</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">buffer</span><span class="p">).</span><span class="nx">toBeInstanceOf</span><span class="p">(</span><span class="nx">Buffer</span><span class="p">);</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">buffer</span><span class="p">.</span><span class="nx">length</span><span class="p">).</span><span class="nx">toBeGreaterThan</span><span class="p">(</span><span class="mi">10000</span><span class="p">);</span> <span class="c1">// At least 10KB</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="consistency-test-same-input--same-output">Consistency Test: Same Input = Same Output</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">should generate identical PDFs for same input</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">order</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getTestOrder</span><span class="p">();</span>

  <span class="kd">const</span> <span class="nx">pdf1</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">pdfService</span><span class="p">.</span><span class="nx">generateInvoicePDF</span><span class="p">(</span><span class="nx">order</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">pdf2</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">pdfService</span><span class="p">.</span><span class="nx">generateInvoicePDF</span><span class="p">(</span><span class="nx">order</span><span class="p">);</span>

  <span class="c1">// Compare checksums</span>
  <span class="kd">const</span> <span class="nx">hash1</span> <span class="o">=</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">createHash</span><span class="p">(</span><span class="dl">'</span><span class="s1">md5</span><span class="dl">'</span><span class="p">).</span><span class="nx">update</span><span class="p">(</span><span class="nx">pdf1</span><span class="p">).</span><span class="nx">digest</span><span class="p">(</span><span class="dl">'</span><span class="s1">hex</span><span class="dl">'</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">hash2</span> <span class="o">=</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">createHash</span><span class="p">(</span><span class="dl">'</span><span class="s1">md5</span><span class="dl">'</span><span class="p">).</span><span class="nx">update</span><span class="p">(</span><span class="nx">pdf2</span><span class="p">).</span><span class="nx">digest</span><span class="p">(</span><span class="dl">'</span><span class="s1">hex</span><span class="dl">'</span><span class="p">);</span>

  <span class="nx">expect</span><span class="p">(</span><span class="nx">hash1</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">hash2</span><span class="p">);</span> <span class="c1">// Must be identical</span>
<span class="p">});</span>
</code></pre></div></div>

<p>This test ensures frontend and backend generate the same PDF.</p>

<h2 id="results--benefits">Results &amp; Benefits</h2>

<p>After implementing this shared PDF package, we saw:</p>

<p><strong>✅ Bug Reduction:</strong></p>
<ul>
  <li>PDF inconsistency bugs: <strong>15 tickets/month → 0 tickets/month</strong></li>
  <li>Data mapping errors: <strong>Eliminated completely</strong></li>
</ul>

<p><strong>✅ Development Speed:</strong></p>
<ul>
  <li>Time to add new PDF template: <strong>2 days → 4 hours</strong> (83% faster)</li>
  <li>Code reuse: <strong>Every component used in 2+ places</strong></li>
</ul>

<p><strong>✅ Maintainability:</strong></p>
<ul>
  <li>Single source of truth for PDF logic</li>
  <li>Type-safe props prevent errors</li>
  <li>Easy to add new templates</li>
</ul>

<p><strong>✅ Developer Experience:</strong></p>
<ul>
  <li>Familiar React syntax</li>
  <li>Hot reload during development</li>
  <li>Clear error messages</li>
</ul>

<h2 id="best-practices--takeaways">Best Practices &amp; Takeaways</h2>

<p><strong>Key Takeaways:</strong></p>

<p>1️⃣ <strong>@react-pdf/renderer works on both browser &amp; Node.js</strong> - Perfect for shared packages</p>

<p>2️⃣ <strong>Monorepo package = one codebase, multiple consumers</strong> - No more duplication</p>

<p>3️⃣ <strong>Data transformer layer is crucial</strong> - Single source of truth for mapping</p>

<p>4️⃣ <strong>Type-safe props catch errors at compile-time</strong> - Better than runtime failures</p>

<p>5️⃣ <strong>Reusable components speed up development</strong> - Build once, use everywhere</p>

<p><strong>Best Practices Checklist:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ Define clear TypeScript interfaces for all props
✅ Build reusable components (Header, Table, Footer)
✅ Use absolute URLs for images (or base64)
✅ Implement data transformer for consistent mapping
✅ Add visual regression tests for PDFs
✅ Lazy load PDF library on frontend (bundle size optimization)
✅ Handle errors gracefully with fallback options
✅ Document component props and usage examples
✅ Version your package properly (semver)
✅ Test on both platforms (browser and Node.js)
</code></pre></div></div>

<h2 id="when-to-use-this-approach">When to Use This Approach</h2>

<p><strong>✅ Use this approach when:</strong></p>
<ul>
  <li>You need PDFs from both frontend AND backend</li>
  <li>You’re already using monorepo (Turborepo, Nx, Lerna)</li>
  <li>Your team is familiar with React</li>
  <li>You need high consistency across platforms</li>
  <li>You have multiple PDF templates to maintain</li>
</ul>

<p><strong>❌ Don’t use this approach when:</strong></p>
<ul>
  <li>You only need PDFs from ONE place (FE or BE)</li>
  <li>PDFs are extremely complex with heavy styling (consider Puppeteer)</li>
  <li>Your team doesn’t know React</li>
  <li>You need pixel-perfect HTML-to-PDF conversion</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Building a shared PDF package might seem like extra work upfront, but it pays dividends in the long run:</p>

<ul>
  <li><strong>No more inconsistencies</strong> between frontend and backend PDFs</li>
  <li><strong>Faster development</strong> with reusable components</li>
  <li><strong>Type safety</strong> prevents bugs before they reach production</li>
  <li><strong>Single source of truth</strong> for all PDF-related logic</li>
</ul>

<p>If you’re generating PDFs in multiple places and struggling with consistency, this approach could be a game-changer for your team.</p>

<p>Hope this helps you build a better PDF generation system! 🎨</p>

<hr />

<p><strong>Resources:</strong></p>
<ul>
  <li><a href="https://react-pdf.org/">@react-pdf/renderer documentation</a></li>
  <li><a href="https://turbo.build/repo/docs">Turborepo monorepo guide</a></li>
  <li><a href="https://www.typescriptlang.org/docs/">TypeScript Handbook</a></li>
</ul>

<p><em>Have you implemented something similar? What challenges did you face? Let me know in the comments!</em></p>]]></content><author><name>Thanh Nguyen</name></author><category term="Backend" /><category term="Full-Stack" /><category term="react-pdf" /><category term="monorepo" /><category term="turborepo" /><category term="pdf-generation" /><category term="typescript" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Email System Migration: From HTML Hell to React Email with Zero Downtime</title><link href="https://beautyoncode.com/backend/architecture/email-migration-react-email-zero-downtime/" rel="alternate" type="text/html" title="Email System Migration: From HTML Hell to React Email with Zero Downtime" /><published>2025-05-19T00:00:00-04:00</published><updated>2025-05-19T00:00:00-04:00</updated><id>https://beautyoncode.com/backend/architecture/email-migration-react-email-zero-downtime</id><content type="html" xml:base="https://beautyoncode.com/backend/architecture/email-migration-react-email-zero-downtime/"><![CDATA[<p><img src="/assets/images/2026/05/2026-05-19-email-migration-cover.webp" alt="" /></p>

<h2 id="table-of-contents">Table of Contents</h2>

<ul>
  <li><a href="#the-pain-of-legacy-email-templates">The Pain of Legacy Email Templates</a></li>
  <li><a href="#the-search-for-a-better-way">The Search for a Better Way</a></li>
  <li><a href="#architecture-the-strategy-pattern-solution">Architecture: The Strategy Pattern Solution</a></li>
  <li><a href="#building-the-react-email-component-library">Building the React Email Component Library</a></li>
  <li><a href="#the-migration-process">The Migration Process</a></li>
  <li><a href="#pitfalls-we-encountered">Pitfalls We Encountered</a></li>
  <li><a href="#the-results">The Results</a></li>
</ul>

<hr />

<h2 id="the-pain-of-legacy-email-templates">The Pain of Legacy Email Templates</h2>

<p>Ever looked at an email template in your codebase that’s just a massive string of HTML with variables sprinkled throughout, and thought: <em>“Who wrote this?”</em></p>

<p>Then you check <code class="language-plaintext highlighter-rouge">git blame</code> and see your own name. 😅</p>

<p>That was my exact situation when I got tasked with “migrate the email system” for a payment platform sending thousands of emails daily.</p>

<p>Today I’m sharing how we migrated our entire email infrastructure from plain HTML strings to React Email - <strong>without any downtime</strong> - and more importantly, without breaking production.</p>

<p>(Spoiler: This isn’t your typical React Email tutorial. This is about <strong>migrating a live system</strong> safely.)</p>

<h2 id="the-legacy-html-hell">The Legacy “HTML Hell”</h2>

<p>Our old system: plain HTML strings with template literals.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">invoiceEmailHtml</span> <span class="o">=</span> <span class="s2">`
  &lt;div&gt;&lt;h1&gt;Invoice #</span><span class="p">${</span><span class="nx">invoiceNumber</span><span class="p">}</span><span class="s2">&lt;/h1&gt;
  &lt;p&gt;Hi </span><span class="p">${</span><span class="nx">customerName</span><span class="p">}</span><span class="s2">,&lt;/p&gt;
  </span><span class="p">${</span><span class="nx">lineItems</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">item</span> <span class="o">=&gt;</span> <span class="s2">`&lt;tr&gt;&lt;td&gt;</span><span class="p">${</span><span class="nx">item</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2">&lt;/td&gt;&lt;/tr&gt;`</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">''</span><span class="p">)}</span><span class="s2">
  &lt;p&gt;Total: $</span><span class="p">${</span><span class="nx">total</span><span class="p">}</span><span class="s2">&lt;/p&gt;&lt;/div&gt;
`</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Problems:</strong> No reusability, no type safety, hard to test.</p>

<p><strong>Production incident:</strong> Bug #1692 - Emails with 10+ products exceeded provider’s 2KB limit and failed silently.</p>

<p>That was our wake-up call.</p>

<h2 id="the-search-for-a-better-way">The Search for a Better Way</h2>

<p>We evaluated: Provider templates (variable limits), Handlebars (maintenance hell), MJML (steep curve).</p>

<p><strong>Winner: React Email</strong></p>
<ul>
  <li>Component-based (reuse everything)</li>
  <li>TypeScript support (catch errors at compile-time)</li>
  <li>Works in browser AND Node.js</li>
  <li>Team already knows React</li>
</ul>

<p>Perfect for our needs.</p>

<h2 id="the-challenge-zero-downtime-migration">The Challenge: Zero-Downtime Migration</h2>

<h3 id="the-big-problem">The Big Problem</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>We're sending 10,000+ emails/day to customers
- Can't stop production
- Can't rewrite everything at once
- Need to test incrementally
- Must be able to rollback quickly
</code></pre></div></div>

<p><strong>The question:</strong> How do we migrate piece by piece without breaking things?</p>

<p><strong>The answer:</strong> <strong>Strategy Pattern</strong></p>

<h2 id="architecture-the-strategy-pattern-solution">Architecture: The Strategy Pattern Solution</h2>

<h3 id="why-strategy-pattern">Why Strategy Pattern?</h3>

<p>Simple if/else doesn’t work when:</p>
<ul>
  <li>You have 10+ email types</li>
  <li>Migration takes months</li>
  <li>Need to mix old + new systems</li>
  <li>Want gradual rollout</li>
</ul>

<p><strong>Strategy Pattern lets you:</strong></p>
<ul>
  <li>Migrate emails one type at a time</li>
  <li>Test new and old side-by-side</li>
  <li>Easy rollback (just change strategy)</li>
</ul>

<blockquote>
  <p><strong>💡 Skip this if:</strong> &lt;5 email types + 1-2 week migration. Just replace all at once.</p>
</blockquote>

<h3 id="the-core-idea">The Core Idea</h3>

<p>Instead of having one way to render emails, we’ll support <strong>three different strategies</strong> simultaneously:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Legacy Strategy: Old HTML strings (existing emails)
2. Provider Strategy: Hosted templates (migration path)
3. Component Strategy: React Email (future goal)
</code></pre></div></div>

<p>All three can coexist. We migrate emails one type at a time.</p>

<h3 id="implementation">Implementation</h3>

<p><strong>Step 1: Define the Interface</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Strategy interface - all rendering methods must implement this</span>
<span class="kr">interface</span> <span class="nx">EmailRenderingStrategy</span> <span class="p">{</span>
  <span class="nx">render</span><span class="p">(</span><span class="nx">payload</span><span class="p">:</span> <span class="nx">EmailPayload</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">EmailRenderOutput</span><span class="o">&gt;</span><span class="p">;</span>
<span class="p">}</span>

<span class="kr">interface</span> <span class="nx">EmailRenderOutput</span> <span class="p">{</span>
  <span class="nl">html</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">subject</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">templateId</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="c1">// For provider-hosted templates</span>
<span class="p">}</span>

<span class="kr">interface</span> <span class="nx">EmailPayload</span> <span class="p">{</span>
  <span class="nl">to</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">data</span><span class="p">:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="kr">any</span><span class="o">&gt;</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Step 2: Implement Three Strategies</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Strategy #1: Legacy HTML (existing system)</span>
<span class="kd">class</span> <span class="nx">InlineFunctionStrategy</span> <span class="k">implements</span> <span class="nx">EmailRenderingStrategy</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nx">render</span><span class="p">(</span><span class="nx">payload</span><span class="p">:</span> <span class="nx">EmailPayload</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">EmailRenderOutput</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="c1">// Call old functions that generate HTML strings</span>
    <span class="kd">const</span> <span class="nx">html</span> <span class="o">=</span> <span class="nx">generateLegacyInvoiceHtml</span><span class="p">(</span><span class="nx">payload</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>

    <span class="k">return</span> <span class="p">{</span>
      <span class="nx">html</span><span class="p">,</span>
      <span class="na">subject</span><span class="p">:</span> <span class="s2">`Invoice #</span><span class="p">${</span><span class="nx">payload</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">invoiceNumber</span><span class="p">}</span><span class="s2">`</span>
    <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Strategy #2: Provider-Hosted Templates (migration path)</span>
<span class="kd">class</span> <span class="nx">ProviderHostedStrategy</span> <span class="k">implements</span> <span class="nx">EmailRenderingStrategy</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nx">render</span><span class="p">(</span><span class="nx">payload</span><span class="p">:</span> <span class="nx">EmailPayload</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">EmailRenderOutput</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span>
      <span class="na">html</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span> <span class="c1">// Provider renders it</span>
      <span class="na">subject</span><span class="p">:</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">subject</span><span class="p">,</span>
      <span class="na">templateId</span><span class="p">:</span> <span class="dl">'</span><span class="s1">invoice-v1</span><span class="dl">'</span> <span class="c1">// ← Resend template ID</span>
    <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Strategy #3: React Email Components (goal)</span>
<span class="kd">class</span> <span class="nx">ComponentBasedStrategy</span> <span class="k">implements</span> <span class="nx">EmailRenderingStrategy</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nx">render</span><span class="p">(</span><span class="nx">payload</span><span class="p">:</span> <span class="nx">EmailPayload</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">EmailRenderOutput</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="c1">// Render React component to HTML</span>
    <span class="kd">const</span> <span class="nx">html</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">renderToString</span><span class="p">(</span>
      <span class="o">&lt;</span><span class="nx">InvoiceEmail</span> <span class="p">{...</span><span class="nx">payload</span><span class="p">.</span><span class="nx">data</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt;
</span>    <span class="p">);</span>

    <span class="k">return</span> <span class="p">{</span>
      <span class="nx">html</span><span class="p">,</span>
      <span class="na">subject</span><span class="p">:</span> <span class="s2">`Invoice #</span><span class="p">${</span><span class="nx">payload</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">invoiceNumber</span><span class="p">}</span><span class="s2">`</span>
    <span class="p">};</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Step 3: Email Service with Strategy Pattern</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">EmailService</span> <span class="p">{</span>
  <span class="k">private</span> <span class="nx">strategies</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="o">&lt;</span><span class="nx">EmailType</span><span class="p">,</span> <span class="nx">EmailRenderingStrategy</span><span class="o">&gt;</span><span class="p">();</span>

  <span class="kd">constructor</span><span class="p">(</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="nx">emailProvider</span><span class="p">:</span> <span class="nx">EmailProviderService</span>
  <span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">registerStrategies</span><span class="p">();</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="nx">registerStrategies</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// Configure which strategy each email type uses</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">strategies</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span>
      <span class="dl">'</span><span class="s1">invoice</span><span class="dl">'</span><span class="p">,</span>
      <span class="k">new</span> <span class="nx">ComponentBasedStrategy</span><span class="p">()</span> <span class="c1">// ← Already migrated</span>
    <span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">strategies</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span>
      <span class="dl">'</span><span class="s1">receipt</span><span class="dl">'</span><span class="p">,</span>
      <span class="k">new</span> <span class="nx">InlineFunctionStrategy</span><span class="p">()</span> <span class="c1">// ← Not migrated yet</span>
    <span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">strategies</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span>
      <span class="dl">'</span><span class="s1">reminder</span><span class="dl">'</span><span class="p">,</span>
      <span class="k">new</span> <span class="nx">ComponentBasedStrategy</span><span class="p">()</span> <span class="c1">// ← Already migrated</span>
    <span class="p">);</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">sendEmail</span><span class="p">(</span><span class="kd">type</span><span class="p">:</span> <span class="nx">EmailType</span><span class="p">,</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">EmailPayload</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="c1">// Get appropriate strategy</span>
    <span class="kd">const</span> <span class="nx">strategy</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">strategies</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="kd">type</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">strategy</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`No strategy found for email type: </span><span class="p">${</span><span class="kd">type</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// Render email using strategy</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">html</span><span class="p">,</span> <span class="nx">subject</span><span class="p">,</span> <span class="nx">templateId</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">strategy</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="nx">payload</span><span class="p">);</span>

    <span class="c1">// Send via provider</span>
    <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">emailProvider</span><span class="p">.</span><span class="nx">send</span><span class="p">({</span>
      <span class="na">to</span><span class="p">:</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">to</span><span class="p">,</span>
      <span class="nx">subject</span><span class="p">,</span>
      <span class="nx">html</span><span class="p">,</span>
      <span class="nx">templateId</span>
    <span class="p">});</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Why This Architecture Rocks:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ Migrate emails one type at a time (invoice first, then receipt, etc.)
✅ Easy rollback (just change the strategy)
✅ Test new and old side-by-side
✅ Zero changes to business logic
✅ Each strategy is independent
✅ Can A/B test different approaches
</code></pre></div></div>

<h2 id="building-the-react-email-component-library">Building the React Email Component Library</h2>

<p>Structure: <code class="language-plaintext highlighter-rouge">emails/</code> for preview, <code class="language-plaintext highlighter-rouge">src/templates/</code> for production code.</p>

<p><strong>Reusable Button Component:</strong></p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/templates/components/Button.tsx</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Button</span> <span class="k">as</span> <span class="nx">EmailButton</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-email/components</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">Button</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">href</span><span class="p">,</span> <span class="nx">children</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">(</span>
  <span class="p">&lt;</span><span class="nc">EmailButton</span> <span class="na">href</span><span class="p">=</span><span class="si">{</span><span class="nx">href</span><span class="si">}</span> <span class="na">style</span><span class="p">=&gt;</span>
    <span class="si">{</span><span class="nx">children</span><span class="si">}</span>
  <span class="p">&lt;/</span><span class="nc">EmailButton</span><span class="p">&gt;</span>
<span class="p">);</span>
</code></pre></div></div>

<p><strong>Invoice Email Template:</strong></p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/templates/InvoiceEmail.tsx</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Html</span><span class="p">,</span> <span class="nx">Body</span><span class="p">,</span> <span class="nx">Container</span><span class="p">,</span> <span class="nx">Text</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-email/components</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Button</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./components/Button</span><span class="dl">'</span><span class="p">;</span>

<span class="kr">interface</span> <span class="nx">InvoiceEmailProps</span> <span class="p">{</span>
  <span class="nl">customerName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">invoiceNumber</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">lineItems</span><span class="p">:</span> <span class="nb">Array</span><span class="o">&lt;</span><span class="p">{</span> <span class="na">description</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">amount</span><span class="p">:</span> <span class="kr">number</span> <span class="p">}</span><span class="o">&gt;</span><span class="p">;</span>
  <span class="nl">total</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="nl">invoiceUrl</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">InvoiceEmail</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">customerName</span><span class="p">,</span> <span class="nx">invoiceNumber</span><span class="p">,</span> <span class="nx">lineItems</span><span class="p">,</span> <span class="nx">total</span><span class="p">,</span> <span class="nx">invoiceUrl</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">(</span>
  <span class="p">&lt;</span><span class="nc">Html</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nc">Body</span> <span class="na">style</span><span class="p">=&gt;</span>
      <span class="p">&lt;</span><span class="nc">Container</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">Text</span><span class="p">&gt;</span>Hi <span class="si">{</span><span class="nx">customerName</span><span class="si">}</span>, Invoice #<span class="si">{</span><span class="nx">invoiceNumber</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">table</span><span class="p">&gt;</span>
          <span class="si">{</span><span class="nx">lineItems</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">item</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span>
            <span class="p">&lt;</span><span class="nt">tr</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">i</span><span class="si">}</span><span class="p">&gt;</span>
              <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">description</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
              <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>$<span class="si">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">amount</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
            <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
          <span class="p">))</span><span class="si">}</span>
        <span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">Text</span><span class="p">&gt;</span>Total: $<span class="si">{</span><span class="nx">total</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nc">Button</span> <span class="na">href</span><span class="p">=</span><span class="si">{</span><span class="nx">invoiceUrl</span><span class="si">}</span><span class="p">&gt;</span>View Invoice<span class="p">&lt;/</span><span class="nc">Button</span><span class="p">&gt;</span>
      <span class="p">&lt;/</span><span class="nc">Container</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nc">Body</span><span class="p">&gt;</span>
  <span class="p">&lt;/</span><span class="nc">Html</span><span class="p">&gt;</span>
<span class="p">);</span>
</code></pre></div></div>

<p><strong>Use in Backend:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">render</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-email/render</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">InvoiceEmail</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../templates/InvoiceEmail</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">html</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">render</span><span class="p">(</span><span class="o">&lt;</span><span class="nx">InvoiceEmail</span> <span class="p">{...</span><span class="nx">invoiceData</span><span class="p">}</span> <span class="sr">/&gt;</span><span class="se">)</span><span class="err">;
</span><span class="k">await</span> <span class="nx">emailService</span><span class="p">.</span><span class="nx">sendEmail</span><span class="p">(</span><span class="dl">'</span><span class="s1">invoice</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">to</span><span class="p">:</span> <span class="nx">email</span><span class="p">,</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="nx">html</span> <span class="p">}</span> <span class="p">});</span>
</code></pre></div></div>

<blockquote>
  <p>See <a href="https://react.email/examples">React Email examples</a> for full templates.</p>
</blockquote>

<h2 id="the-migration-process">The Migration Process</h2>

<h3 id="phase-1-preview-server">Phase 1: Preview Server</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run dev  <span class="c"># Start preview at localhost:3000</span>
</code></pre></div></div>

<p>Preview all templates locally with hot reload - no more “send and check Gmail”!</p>

<h3 id="phase-2-migrate-by-priority">Phase 2: Migrate by Priority</h3>

<p><strong>Order:</strong></p>
<ol>
  <li>High-volume emails with bugs (invoices)</li>
  <li>Customer-facing emails (confirmations)</li>
  <li>Internal emails (reports)</li>
  <li>Low-volume emails (password reset)</li>
</ol>

<p><strong>Per Email Type:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Build React component (4 hours)
2. Test in preview + send to Gmail/Outlook/Apple Mail (1 hour)
3. Deploy to staging (30 min)
4. Deploy to production (30 min)
5. Monitor for 24 hours
</code></pre></div></div>

<h3 id="phase-3-feature-flags-optional">Phase 3: Feature Flags (Optional)</h3>

<p>For high-risk migrations, use environment variable:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">useReactEmail</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">REACT_EMAIL_ENABLED</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">true</span><span class="dl">'</span><span class="p">;</span>

<span class="k">this</span><span class="p">.</span><span class="nx">strategies</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span>
  <span class="dl">'</span><span class="s1">invoice</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">useReactEmail</span> <span class="p">?</span> <span class="k">new</span> <span class="nx">ComponentBasedStrategy</span><span class="p">()</span> <span class="p">:</span> <span class="k">new</span> <span class="nx">InlineFunctionStrategy</span><span class="p">()</span>
<span class="p">);</span>
</code></pre></div></div>

<p><strong>Rollout:</strong> OFF → Staging ON → Production ON → Remove flag after 7 days.</p>

<p><strong>Monitor:</strong> Delivery rate (99.9%), bounce rate (0.1%), render time (&lt;500ms).</p>

<p><strong>Rollback if any metric degrades by &gt;1%.</strong></p>

<h2 id="pitfalls-we-encountered">Pitfalls We Encountered</h2>

<h3 id="pitfall-1-not-testing-across-email-clients-early">Pitfall #1: Not Testing Across Email Clients Early</h3>

<p><strong>Mistake:</strong> Built all React Email components, tested only in Gmail, deployed to production.</p>

<p><strong>Result:</strong> Outlook users saw broken layouts (Word rendering engine), Apple Mail didn’t load logos.</p>

<p><strong>Lesson:</strong> Test in Gmail, Outlook, Apple Mail BEFORE deploying each template. 30 minutes of testing saves days of emergency fixes.</p>

<p><strong>Common fixes:</strong></p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Outlook doesn't support flexbox</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">style</span><span class="p">=&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>

<span class="c1">// ✅ Use tables instead</span>
<span class="p">&lt;</span><span class="nt">table</span> <span class="na">width</span><span class="p">=</span><span class="s">"100%"</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;&lt;</span><span class="nt">td</span> <span class="na">width</span><span class="p">=</span><span class="s">"50%"</span><span class="p">&gt;</span>Column 1<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;&lt;</span><span class="nt">td</span><span class="p">&gt;</span>Column 2<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>

<span class="c1">// ✅ Add alt text for images (Gmail blocks by default)</span>
<span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="p">=</span><span class="s">"https://cdn.example.com/logo.png"</span> <span class="na">alt</span><span class="p">=</span><span class="s">"Company Logo"</span> <span class="p">/&gt;</span>
</code></pre></div></div>

<p><strong>Tools:</strong> Litmus ($99/mo), Email on Acid ($99/mo), or send to real test accounts (free).</p>

<h3 id="pitfall-2-image-paths">Pitfall #2: Image Paths</h3>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Relative paths don't work in emails</span>
<span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="p">=</span><span class="s">"/assets/logo.png"</span> <span class="p">/&gt;</span>

<span class="c1">// ✅ Use absolute URLs</span>
<span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="p">=</span><span class="s">"https://cdn.example.com/logo.png"</span> <span class="p">/&gt;</span>
</code></pre></div></div>

<h3 id="pitfall-3-css-limitations">Pitfall #3: CSS Limitations</h3>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ CSS classes don't work</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"card"</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>

<span class="c1">// ✅ Use inline styles</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">style</span><span class="p">=&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>

<span class="c1">// ✅ Use tables for layout</span>
<span class="p">&lt;</span><span class="nt">table</span><span class="p">&gt;&lt;</span><span class="nt">tr</span><span class="p">&gt;&lt;</span><span class="nt">td</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;&lt;/</span><span class="nt">tr</span><span class="p">&gt;&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>
</code></pre></div></div>

<h3 id="pitfall-4-large-template-bundles">Pitfall #4: Large Template Bundles</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Lazy load to avoid importing React Email on every request</span>
<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">sendEmail</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">render</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">@react-email/render</span><span class="dl">'</span><span class="p">);</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">InvoiceEmail</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">@my-company/email-templates</span><span class="dl">'</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">html</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">render</span><span class="p">(</span><span class="o">&lt;</span><span class="nx">InvoiceEmail</span> <span class="p">{...</span><span class="nx">props</span><span class="p">}</span> <span class="sr">/&gt;</span><span class="se">)</span><span class="err">;
</span><span class="p">}</span>
</code></pre></div></div>

<h2 id="the-results">The Results</h2>

<p>After 3 months of migration:</p>

<h3 id="metrics-that-improved">Metrics That Improved</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ Email delivery rate: 97.5% → 99.9%
✅ Bugs related to emails: 15 tickets/month → 1 ticket/month
✅ Development time: -60% (component reuse)
✅ Test coverage: 40% → 85%
✅ Time to add new email: 2 days → 4 hours
</code></pre></div></div>

<h3 id="developer-experience">Developer Experience</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ Component library → Consistent design
✅ TypeScript → Catch errors at compile time
✅ Preview server → No more "send and check Gmail"
✅ Hot reload → Instant feedback
✅ Version control → Track template changes in git
✅ Reusable components → Write once, use everywhere
</code></pre></div></div>

<h3 id="business-impact">Business Impact</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ Customer satisfaction up (better email experience)
✅ Support tickets down (fewer email issues)
✅ New email templates ship faster
✅ Design consistency across all emails
</code></pre></div></div>

<h2 id="key-takeaways">Key Takeaways</h2>

<p>1️⃣ <strong>Don’t rewrite everything at once</strong> - Migrate incrementally using Strategy Pattern</p>

<p>2️⃣ <strong>Strategy Pattern enables parallel approaches</strong> - Old and new can coexist</p>

<p>3️⃣ <strong>Feature flags are your best friend</strong> - Easy rollout and rollback</p>

<p>4️⃣ <strong>Monitor metrics before committing 100%</strong> - Catch issues early</p>

<p>5️⃣ <strong>Component-based approach pays long-term dividends</strong> - Reusability saves massive time</p>

<p>6️⃣ <strong>Preview server is a game-changer</strong> - Speeds up development by 10x</p>

<p>7️⃣ <strong>TypeScript catches bugs before production</strong> - Type-safe templates prevent errors</p>

<p>8️⃣ <strong>Test in multiple email clients</strong> - Gmail, Outlook, Apple Mail all render differently</p>

<hr />

<h2 id="whats-next">What’s Next?</h2>

<p>After successfully migrating your email system, consider these follow-up topics:</p>

<h3 id="-testing--quality">🧪 <strong>Testing &amp; Quality</strong></h3>
<p>If your email templates are critical (invoices, receipts), invest in automated testing:</p>
<ul>
  <li>Automated screenshot testing across email clients (Litmus, Email on Acid)</li>
  <li>Visual regression testing (catch unintended style changes)</li>
  <li>Load testing (can your system handle 10K+ emails/hour?)</li>
</ul>

<h3 id="️-production-infrastructure">🏗️ <strong>Production Infrastructure</strong></h3>
<p>For high-volume email systems (&gt;100K emails/day):</p>
<ul>
  <li>Queue-based processing with Bull/BullMQ (handle spikes, retries)</li>
  <li>Email provider failover (Resend → SendGrid backup)</li>
  <li>Rate limiting to avoid provider throttling</li>
</ul>

<h3 id="-security--compliance">🔐 <strong>Security &amp; Compliance</strong></h3>
<p>Before going to production at scale:</p>
<ul>
  <li>SPF, DKIM, DMARC configuration (prevent your emails from being marked as spam)</li>
  <li>GDPR compliance for email data retention</li>
  <li>Preventing email injection attacks</li>
</ul>

<p>Want to learn more? Check out the <a href="https://react.email/docs/introduction">React Email documentation</a> for advanced patterns.</p>

<h2 id="best-practices-checklist">Best Practices Checklist</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Pre-Migration:
✅ Set up preview server for local development
✅ Build component library with reusable parts
✅ Define TypeScript interfaces for all email props
✅ Implement Strategy Pattern in email service
✅ Create feature flags for gradual rollout

During Migration:
✅ Migrate high-priority emails first
✅ Test thoroughly in preview before deploying
✅ Use feature flags for A/B testing
✅ Monitor delivery rates and error logs
✅ Keep old code for quick rollback

Post-Migration:
✅ Remove old code only after 100% rollout
✅ Document component usage for team
✅ Set up automated screenshot tests
✅ Create email template guidelines
✅ Celebrate with team! 🎉
</code></pre></div></div>

<h2 id="when-should-you-migrate">When Should You Migrate?</h2>

<p><strong>✅ Migrate to React Email if:</strong></p>
<ul>
  <li>Your team knows React</li>
  <li>You have multiple email templates</li>
  <li>You need design consistency</li>
  <li>You want type safety</li>
  <li>You have complex dynamic emails</li>
  <li>You’re tired of email bugs</li>
</ul>

<p><strong>❌ Don’t migrate if:</strong></p>
<ul>
  <li>You only have 1-2 simple emails</li>
  <li>Team doesn’t know React</li>
  <li>Email templates rarely change</li>
  <li>Current system works well</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Migrating a production email system is scary, but with the right architecture (Strategy Pattern), proper planning (incremental migration), and good tooling (React Email), it’s totally manageable.</p>

<p>The key lessons:</p>
<ul>
  <li><strong>Don’t change everything at once</strong></li>
  <li><strong>Make it reversible</strong></li>
  <li><strong>Monitor closely</strong></li>
  <li><strong>Invest in reusable components</strong></li>
</ul>

<p>If you’re struggling with unmaintainable email templates, this approach could transform your email system from a maintenance nightmare into a joy to work with.</p>

<p>Hope this helps you build a better email infrastructure! 📧</p>

<hr />

<p><strong>Resources:</strong></p>
<ul>
  <li><a href="https://react.email/docs/introduction">React Email Documentation</a></li>
  <li><a href="https://react.email/docs/getting-started/monorepo-setup/pnpm">React Email - Monorepo Setup</a> (for pnpm/Turborepo/Nx)</li>
  <li><a href="https://refactoring.guru/design-patterns/strategy">Strategy Pattern Explained</a></li>
  <li><a href="https://www.emailonacid.com/">Email on Acid - Testing Tools</a></li>
</ul>

<p><em>Have you migrated an email system before? What challenges did you face? Share your experience in the comments!</em></p>]]></content><author><name>Thanh Nguyen</name></author><category term="Backend" /><category term="Architecture" /><category term="email" /><category term="react-email" /><category term="nestjs" /><category term="migration" /><category term="strategy-pattern" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Một Vài Lưu Ý Khi Lập Lịch Hàng Tháng với EventBridge</title><link href="https://beautyoncode.com/aws/even-bridge-scheduler/" rel="alternate" type="text/html" title="Một Vài Lưu Ý Khi Lập Lịch Hàng Tháng với EventBridge" /><published>2024-12-10T00:00:00-05:00</published><updated>2024-12-10T00:00:00-05:00</updated><id>https://beautyoncode.com/aws/even-bridge-scheduler</id><content type="html" xml:base="https://beautyoncode.com/aws/even-bridge-scheduler/"><![CDATA[<p><img src="/assets/images/2024/12/2024-12-10-even-bridge-scheduler-cover.jpg" alt="" /></p>

<p>Trong bài viết hôm nay, mình sẽ chia sẻ về một chủ đề đơn giản nhưng thú vị: <strong>lập lịch công việc định kỳ (scheduling)</strong>. Hãy tưởng tượng bạn cần tự động tặng thêm $10 hàng tháng cho khách hàng đã đăng ký gói Premium của dịch vụ bạn cung cấp.</p>

<p>Với AWS, có hai giải pháp phổ biến để giải quyết bài toán này: <strong>SQS Queue</strong> và <strong>EventBridge</strong>. Trong bài viết này, mình sẽ tập trung vào giải pháp sử dụng <strong>Amazon EventBridge Scheduler</strong>.</p>

<p>Để bạn dễ hình dung bài toán, dưới đây là <a href="https://drive.google.com/file/d/1zsuc1sh_sLDmxJxjsJRunB42yioDdwdm/view?usp=drive_link"><strong>Subscription-Based Payment Flow with AWS EventBridge and Stripe</strong></a>:
<img src="/assets/images/2024/12/2024-12-10-even-bridge-scheduler-flow-1.png" alt="" /></p>

<p><strong>Mô tả flow:</strong></p>

<ul>
  <li>(1) Người dùng muốn mua gói <strong>subscription Premium</strong> theo hình thức thanh toán <strong>hàng năm</strong>.</li>
  <li>(2) Ứng dụng Frontend gọi đến Backend để lấy thông tin phiên checkout của Stripe.</li>
  <li>(3) Ứng dụng FE nhận được URL phiên checkout và chuyển hướng người dùng đến giao diện Stripe để bắt đầu thanh toán.</li>
  <li>(4) Người dùng điền thông tin và thanh toán thành công.</li>
  <li>(5) Stripe kích hoạt một Webhook event thông báo thanh toán thành công, một gói đăng ký mới được kích hoạt.</li>
  <li>(6) Ứng dụng BE lắng nghe sự kiện và tạo một Event Bridge Scheduler sẽ chạy <strong>hàng tháng</strong>, bắt đầu từ thời gian hiện tại.</li>
  <li>(7) Event Bridge Scheduler kích hoạt sự kiện theo lịch (hàng tháng), hàm Lambda được cài đặt làm mục tiêu sẽ chạy.</li>
  <li>(8) Lambda xử lý logic công việc và cập nhật cơ sở dữ liệu (nếu có).</li>
</ul>

<p>Bài viết này sẽ tập trung phân tích cách lập lịch hàng tháng với EventBridge Scheduler trong <strong>flow 6 và 7: Schedule Monthly Task with EventBridge</strong></p>

<h2 id="amazon-eventbridge-là-gì">Amazon EventBridge là gì?</h2>
<p><a href="https://aws.amazon.com/eventbridge/">Amazon EventBridge</a> là một dịch vụ serverless của AWS, giúp bạn dễ dàng kết nối các ứng dụng thông qua các sự kiện (event-driven). EventBridge hỗ trợ lập lịch công việc và chuyển tiếp sự kiện từ nhiều nguồn khác nhau (AWS services, SaaS apps, custom applications) đến các mục tiêu như Lambda, SQS, Step Functions…</p>

<p>Bạn có thể xem thêm video giới thiệu ngắn gọn này:</p>
<iframe width="640" height="360" src="https://www.youtube.com/embed/5K6qpMOVS0E?si=XaQ5r_pPQI-1yD7y" frameborder="0" allowfullscreen=""></iframe>

<h2 id="amazon-eventbridge-scheduler-là-gì">Amazon EventBridge Scheduler là gì?</h2>

<p><strong>Amazon EventBridge Scheduler</strong> là một tính năng được tích hợp sẵn trong EventBridge, cho phép lập lịch và thực thi các công việc định kỳ hoặc vào các thời điểm cụ thể trong tương lai. Nó giúp tự động hóa các tác vụ như gửi thông báo, kích hoạt Lambda function, chạy Step Functions, và gửi sự kiện đến các dịch vụ khác trong AWS theo lịch trình được xác định trước.</p>

<p>Các tính năng chính:</p>
<ul>
  <li>Lập lịch công việc định kỳ.</li>
  <li>Hỗ trợ các loại khác nhau: Rate-based, Cron-based, One-time.</li>
  <li>Tích hợp với các dịch vụ AWS như Lambda, Step Functions, SQS. …</li>
  <li>Quản lý lịch trình dễ dàng qua AWS Management Console, SDK, CLI, API</li>
</ul>

<h2 id="phân-loại-lịch-trong-eventbridge-scheduler">Phân Loại Lịch trong EventBridge Scheduler</h2>

<p>EventBridge Scheduler hỗ trợ 3 loại schedule:</p>

<h3 id="rate-based-schedule-định-kỳ-theo-chu-kỳ"><strong>Rate-based</strong> schedule (Định kỳ theo chu kỳ):</h3>
<ul>
  <li>Dùng để trigger event theo khoảng thời gian cố định.</li>
  <li>Cú pháp: <code class="language-plaintext highlighter-rouge">rate(value unit)</code> với value là số dương và unit là minutes, hours, hoặc days.</li>
  <li>Ví dụ: <code class="language-plaintext highlighter-rouge">rate(5 minutes)</code> sẽ trigger event mỗi 5 phút.</li>
</ul>

<p><img src="/assets/images/2024/12/2024-12-10-even-bridge-scheduler-rate.png" alt="" /></p>

<h3 id="cron-based-schedule-định-kỳ-theo-lịch-cụ-thể"><strong>Cron-based</strong> schedule (Định kỳ theo lịch cụ thể):</h3>
<ul>
  <li>Dùng để trigger event vào thời gian cụ thể trong ngày, tuần, tháng hoặc năm.</li>
  <li>Cú pháp: <code class="language-plaintext highlighter-rouge">cron(minutes hours day-of-month month day-of-week year)</code></li>
  <li>Ví dụ: <code class="language-plaintext highlighter-rouge">cron(0 0 1 * ? *)</code> sẽ trigger vào 0h ngày 1 mỗi tháng.</li>
</ul>

<p><img src="/assets/images/2024/12/2024-12-10-even-bridge-scheduler-cron.png" alt="" /></p>

<h3 id="one-time-schedule-chỉ-định-một-lần"><strong>One-time</strong> schedule (chỉ định một lần):</h3>
<ul>
  <li>Trigger sự kiện duy nhất vào một thời điểm cụ thể.</li>
</ul>

<p><img src="/assets/images/2024/12/2024-12-10-even-bridge-scheduler-one-time-schedule.png" alt="" /></p>

<h2 id="phân-tích-bài-toán-lập-lịch-hàng-tháng">Phân Tích Bài Toán Lập Lịch Hàng Tháng</h2>

<p>Giả sử bạn cần tự động thực hiện tác vụ trên (tặng $10 vào tài khoản) hàng tháng vào đúng ngày mua “Premium subscription” của khách hàng, dưới đây là phân tích về hai phương pháp phổ biến:</p>

<h3 id="rate-based-schedule">Rate-based Schedule</h3>
<ul>
  <li><strong>Rate expression</strong>: <code class="language-plaintext highlighter-rouge">rate(30 days)</code></li>
  <li><strong>Hạn chế</strong>: Tháng có thể dài hơn hoặc ngắn hơn 30 ngày (ví dụ tháng 2 có 28 ngày). Do đó, việc sử dụng <code class="language-plaintext highlighter-rouge">rate(30 days)</code> sẽ gây ra sự chênh lệch, khiến người dùng nhận thông báo hoặc hành động không đúng ngày mong muốn.</li>
</ul>

<h3 id="cron-based-schedule">Cron-based Schedule</h3>
<ul>
  <li><strong>Cron expression</strong>: <code class="language-plaintext highlighter-rouge">cron(0 0 x * ? *)</code>
    <ul>
      <li>trigger event mỗi tháng vào ngày <code class="language-plaintext highlighter-rouge">x</code> (ngày mua Premium subscription).</li>
    </ul>
  </li>
  <li><strong>Hạn chế</strong>:
    <ul>
      <li>Nếu người dùng mua gói vào ngày <strong>31</strong>, cron sẽ <em>không thể</em> trigger trong các tháng không có ngày 31 (tháng 2, 4, 6, 9, 11).</li>
      <li>Tương tự cho các ngày 29, 30 thì tháng 2 sẽ bị thiếu events.</li>
    </ul>
  </li>
</ul>

<p><img src="/assets/images/2024/12/2024-12-10-even-bridge-scheduler-cron-missing-event.png" alt="" /></p>

<h2 id="giải-pháp-xử-lý-cron-based-schedule">Giải Pháp Xử Lý Cron-based Schedule</h2>
<h3 id="sử-dụng-ngày-đầu-tháng-tiếp-theo">Sử Dụng Ngày Đầu Tháng Tiếp Theo</h3>
<ul>
  <li>Đặt cron chuyển các ngày lớn hơn 28 thành ngày 1 của tháng tiếp theo.</li>
  <li>Cách này đơn giản, đảm bảo mỗi tháng đều có event trigger, nhưng có thể gây trễ 1-2 ngày với người dùng.</li>
  <li>Ví dụ:
    <ul>
      <li>Ngày <code class="language-plaintext highlighter-rouge">≤ 28</code>: <code class="language-plaintext highlighter-rouge">cron(0 0 x * ? *)</code></li>
      <li>Ngày <code class="language-plaintext highlighter-rouge">&gt; 28</code>: <code class="language-plaintext highlighter-rouge">cron(0 0 1 * ? *)</code></li>
    </ul>
  </li>
</ul>

<h3 id="sử-dụng-one-time-schedule">Sử Dụng One-time Schedule</h3>
<ul>
  <li>Với mỗi lần trigger, Lambda function sẽ tạo ra lịch trigger tiếp theo dựa trên logic tính toán ngày cuối cùng của tháng.</li>
  <li>Ví dụ: nếu user mua gói Premium vào ngày 31/12/2024, các lần trigger tiếp theo sẽ là 31/01/2025, 28/02/2025, 31/03/2025. Viết logic code để chọn đúng ngày tiếp theo và tạo one-time schedule tương ứng.</li>
</ul>

<h2 id="giải-pháp-xử-lý-online-payment-tương-ứng-với-scheduler">Giải Pháp Xử Lý Online Payment Tương Ứng với Scheduler</h2>
<p>Sau khi chọn được loại schedule phù hợp với mình là “Sử Dụng Ngày Đầu Tháng Tiếp Theo”, mình cần xử lý logic tương ứng với sự kiện Stripe Payment.</p>

<p>Ví dụ:</p>
<ul>
  <li>Nếu người dùng thanh toán vào ngày <code class="language-plaintext highlighter-rouge">28/2/2024</code> nhưng bạn chọn <code class="language-plaintext highlighter-rouge">cron(0 0 1 * ? *)</code>, sự kiện sẽ trigger vào ngày <code class="language-plaintext highlighter-rouge">01/03/2024</code> (trễ <code class="language-plaintext highlighter-rouge">1</code> ngày).</li>
  <li>Nếu thanh toán vào ngày <code class="language-plaintext highlighter-rouge">30/10/2024</code> nhưng bạn chọn <code class="language-plaintext highlighter-rouge">cron(0 0 1 * ? *)</code>, sự kiện sẽ trigger vào ngày <code class="language-plaintext highlighter-rouge">01/11/2024</code> (trễ 2 ngày).</li>
</ul>

<p>Nếu đó là chương trình khuyến mãi, việc trễ 1-2 ngày là có thể chấp nhận được. Tuy nhiên, nếu tính chất công việc quan trọng yêu cầu chính xác cao, thì hướng tiếp cận này chưa phải là tối ưu nhất.</p>

<p>Với Stripe, có một tính năng là <a href="https://docs.stripe.com/billing/subscriptions/prorations#when-prorations-are-applied">Prorations</a> - cài đặt <code class="language-plaintext highlighter-rouge">"proration_behavior": "none"</code> khi tạo checkout session, người dùng sẽ không trả chi phí cho 1-2 ngày gap giữa ngày mua subscription và ngày trigger event quan trọng, vì thanh toán thực tế sẽ diễn ra vào ngày 1 của tháng tiếp theo.</p>

<p>Điều này giúp bạn giữ chính xác về ngày trigger event cũng như đảm bảo quyền lợi cho người dùng.</p>

<h2 id="kết-luận">Kết Luận</h2>
<p>Khi lập lịch công việc định kỳ với AWS EventBridge, bạn cần cân nhắc kỹ các trường hợp đặc biệt như:</p>

<ul>
  <li>Các tháng không có đủ số ngày trong cron expression.</li>
  <li>Sự khác biệt về số ngày giữa các tháng.</li>
</ul>

<p>Hy vọng bài viết này giúp bạn hiểu rõ hơn cách dùng EventBridge Scheduler và cách chọn schedule type phù hợp với bài toán của mình.</p>

<p><strong>Đọc thêm:</strong></p>
<ul>
  <li><a href="https://aws.amazon.com/eventbridge/">Amazon Event Bridge</a></li>
  <li><a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html">Schedule types in EventBridge Scheduler</a></li>
  <li><a href="https://docs.stripe.com/billing/subscriptions/prorations#when-prorations-are-applied">Stripe - Prorations</a></li>
  <li><a href="https://docs.stripe.com/api/checkout/sessions">Stripe - Checkout Session</a></li>
</ul>]]></content><author><name>Thanh Nguyen</name></author><category term="AWS" /><category term="aws" /><category term="eventbridge" /><summary type="html"><![CDATA[Trong bài viết hôm nay, mình sẽ chia sẻ về một chủ đề đơn giản nhưng thú vị: lập lịch công việc định kỳ (scheduling). Hãy tưởng tượng bạn cần tự động tặng thêm $10 hàng tháng cho khách hàng đã đăng ký gói Premium của dịch vụ bạn cung cấp. Với AWS, có hai giải pháp phổ biến để giải quyết bài toán này: SQS Queue và EventBridge. Trong bài viết này, mình sẽ tập trung vào giải pháp sử dụng Amazon EventBridge Scheduler. Để bạn dễ hình dung bài toán, dưới đây là Subscription-Based Payment Flow with AWS EventBridge and Stripe: Mô tả flow: (1) Người dùng muốn mua gói subscription Premium theo hình thức thanh toán hàng năm. (2) Ứng dụng Frontend gọi đến Backend để lấy thông tin phiên checkout của Stripe. (3) Ứng dụng FE nhận được URL phiên checkout và chuyển hướng người dùng đến giao diện Stripe để bắt đầu thanh toán. (4) Người dùng điền thông tin và thanh toán thành công. (5) Stripe kích hoạt một Webhook event thông báo thanh toán thành công, một gói đăng ký mới được kích hoạt. (6) Ứng dụng BE lắng nghe sự kiện và tạo một Event Bridge Scheduler sẽ chạy hàng tháng, bắt đầu từ thời gian hiện tại. (7) Event Bridge Scheduler kích hoạt sự kiện theo lịch (hàng tháng), hàm Lambda được cài đặt làm mục tiêu sẽ chạy. (8) Lambda xử lý logic công việc và cập nhật cơ sở dữ liệu (nếu có). Bài viết này sẽ tập trung phân tích cách lập lịch hàng tháng với EventBridge Scheduler trong flow 6 và 7: Schedule Monthly Task with EventBridge Amazon EventBridge là gì? Amazon EventBridge là một dịch vụ serverless của AWS, giúp bạn dễ dàng kết nối các ứng dụng thông qua các sự kiện (event-driven). EventBridge hỗ trợ lập lịch công việc và chuyển tiếp sự kiện từ nhiều nguồn khác nhau (AWS services, SaaS apps, custom applications) đến các mục tiêu như Lambda, SQS, Step Functions… Bạn có thể xem thêm video giới thiệu ngắn gọn này: Amazon EventBridge Scheduler là gì? Amazon EventBridge Scheduler là một tính năng được tích hợp sẵn trong EventBridge, cho phép lập lịch và thực thi các công việc định kỳ hoặc vào các thời điểm cụ thể trong tương lai. Nó giúp tự động hóa các tác vụ như gửi thông báo, kích hoạt Lambda function, chạy Step Functions, và gửi sự kiện đến các dịch vụ khác trong AWS theo lịch trình được xác định trước. Các tính năng chính: Lập lịch công việc định kỳ. Hỗ trợ các loại khác nhau: Rate-based, Cron-based, One-time. Tích hợp với các dịch vụ AWS như Lambda, Step Functions, SQS. … Quản lý lịch trình dễ dàng qua AWS Management Console, SDK, CLI, API Phân Loại Lịch trong EventBridge Scheduler EventBridge Scheduler hỗ trợ 3 loại schedule: Rate-based schedule (Định kỳ theo chu kỳ): Dùng để trigger event theo khoảng thời gian cố định. Cú pháp: rate(value unit) với value là số dương và unit là minutes, hours, hoặc days. Ví dụ: rate(5 minutes) sẽ trigger event mỗi 5 phút. Cron-based schedule (Định kỳ theo lịch cụ thể): Dùng để trigger event vào thời gian cụ thể trong ngày, tuần, tháng hoặc năm. Cú pháp: cron(minutes hours day-of-month month day-of-week year) Ví dụ: cron(0 0 1 * ? *) sẽ trigger vào 0h ngày 1 mỗi tháng. One-time schedule (chỉ định một lần): Trigger sự kiện duy nhất vào một thời điểm cụ thể. Phân Tích Bài Toán Lập Lịch Hàng Tháng Giả sử bạn cần tự động thực hiện tác vụ trên (tặng $10 vào tài khoản) hàng tháng vào đúng ngày mua “Premium subscription” của khách hàng, dưới đây là phân tích về hai phương pháp phổ biến: Rate-based Schedule Rate expression: rate(30 days) Hạn chế: Tháng có thể dài hơn hoặc ngắn hơn 30 ngày (ví dụ tháng 2 có 28 ngày). Do đó, việc sử dụng rate(30 days) sẽ gây ra sự chênh lệch, khiến người dùng nhận thông báo hoặc hành động không đúng ngày mong muốn. Cron-based Schedule Cron expression: cron(0 0 x * ? *) trigger event mỗi tháng vào ngày x (ngày mua Premium subscription). Hạn chế: Nếu người dùng mua gói vào ngày 31, cron sẽ không thể trigger trong các tháng không có ngày 31 (tháng 2, 4, 6, 9, 11). Tương tự cho các ngày 29, 30 thì tháng 2 sẽ bị thiếu events. Giải Pháp Xử Lý Cron-based Schedule Sử Dụng Ngày Đầu Tháng Tiếp Theo Đặt cron chuyển các ngày lớn hơn 28 thành ngày 1 của tháng tiếp theo. Cách này đơn giản, đảm bảo mỗi tháng đều có event trigger, nhưng có thể gây trễ 1-2 ngày với người dùng. Ví dụ: Ngày ≤ 28: cron(0 0 x * ? *) Ngày &gt; 28: cron(0 0 1 * ? *) Sử Dụng One-time Schedule Với mỗi lần trigger, Lambda function sẽ tạo ra lịch trigger tiếp theo dựa trên logic tính toán ngày cuối cùng của tháng. Ví dụ: nếu user mua gói Premium vào ngày 31/12/2024, các lần trigger tiếp theo sẽ là 31/01/2025, 28/02/2025, 31/03/2025. Viết logic code để chọn đúng ngày tiếp theo và tạo one-time schedule tương ứng. Giải Pháp Xử Lý Online Payment Tương Ứng với Scheduler Sau khi chọn được loại schedule phù hợp với mình là “Sử Dụng Ngày Đầu Tháng Tiếp Theo”, mình cần xử lý logic tương ứng với sự kiện Stripe Payment. Ví dụ: Nếu người dùng thanh toán vào ngày 28/2/2024 nhưng bạn chọn cron(0 0 1 * ? *), sự kiện sẽ trigger vào ngày 01/03/2024 (trễ 1 ngày). Nếu thanh toán vào ngày 30/10/2024 nhưng bạn chọn cron(0 0 1 * ? *), sự kiện sẽ trigger vào ngày 01/11/2024 (trễ 2 ngày). Nếu đó là chương trình khuyến mãi, việc trễ 1-2 ngày là có thể chấp nhận được. Tuy nhiên, nếu tính chất công việc quan trọng yêu cầu chính xác cao, thì hướng tiếp cận này chưa phải là tối ưu nhất. Với Stripe, có một tính năng là Prorations - cài đặt "proration_behavior": "none" khi tạo checkout session, người dùng sẽ không trả chi phí cho 1-2 ngày gap giữa ngày mua subscription và ngày trigger event quan trọng, vì thanh toán thực tế sẽ diễn ra vào ngày 1 của tháng tiếp theo. Điều này giúp bạn giữ chính xác về ngày trigger event cũng như đảm bảo quyền lợi cho người dùng. Kết Luận Khi lập lịch công việc định kỳ với AWS EventBridge, bạn cần cân nhắc kỹ các trường hợp đặc biệt như: Các tháng không có đủ số ngày trong cron expression. Sự khác biệt về số ngày giữa các tháng. Hy vọng bài viết này giúp bạn hiểu rõ hơn cách dùng EventBridge Scheduler và cách chọn schedule type phù hợp với bài toán của mình. Đọc thêm: Amazon Event Bridge Schedule types in EventBridge Scheduler Stripe - Prorations Stripe - Checkout Session]]></summary></entry><entry><title type="html">Relation fields in Django Rest Framework Serializer</title><link href="https://beautyoncode.com/django/django%20rest%20framework%20(drf)/relation-fields-in-drf-serializer/" rel="alternate" type="text/html" title="Relation fields in Django Rest Framework Serializer" /><published>2023-10-24T00:00:00-04:00</published><updated>2023-10-24T00:00:00-04:00</updated><id>https://beautyoncode.com/django/django%20rest%20framework%20(drf)/relation-fields-in-drf-serializer</id><content type="html" xml:base="https://beautyoncode.com/django/django%20rest%20framework%20(drf)/relation-fields-in-drf-serializer/"><![CDATA[<p><img src="/assets/images/2023/10/2023-10-relation-fields-in-drf-serializer-cover.png" alt="" />
The Django model offers various types of relationships such as OneToOneField, ForeignKey, ManyToManyField, and GenericForeignKey.</p>

<p>To present or write data of relationship in a serializer, you can utilize DRF Relation fields.</p>

<p>In this post, I will summarize the key points of relational fields and then delve into customizing a relation field to facilitate reading and writing relationship data.</p>

<p>Although the <a href="https://www.django-rest-framework.org/api-guide/relations/#serializer-relations">official document</a> mentions this custom relational topic, it lacks examples and use cases. Therefore, I aim to make it more practical by providing relevant illustrations.</p>

<p>Before we delve into the content, let’s take a look at the relevant models:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Album</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span>
    <span class="n">album_name</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>

<span class="k">class</span> <span class="nc">Track</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span>
    <span class="n">album</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">ForeignKey</span><span class="p">(</span>
        <span class="n">Album</span><span class="p">,</span>
        <span class="n">related_name</span><span class="o">=</span><span class="s">'tracks'</span><span class="p">,</span>
        <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="p">.</span><span class="n">CASCADE</span>
    <span class="p">)</span>
    <span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>
    <span class="n">duration</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">IntegerField</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="performance-concerns-related-to-relation-fields-and-the-responsibility-of-developers">Performance concerns related to relation fields and the responsibility of developers</h2>

<p>When using Django REST Framework (DRF), it is important to note that DRF <strong>does not automatically optimize the queryset that is passed to the serializer</strong>.</p>

<p>It is the responsibility of the developer to optimize the performance of relation fields in DRF. By using methods like prefetch_related and select_related, developers can improve the efficiency of their queries and enhance the overall performance of their applications.</p>

<p>With above models, if we have <em>AlbumSerializer</em>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AlbumSerializer</span><span class="p">(</span><span class="n">serializers</span><span class="p">.</span><span class="n">ModelSerializer</span><span class="p">):</span>
    <span class="n">tracks</span> <span class="o">=</span> <span class="n">serializers</span><span class="p">.</span><span class="n">StringRelatedField</span><span class="p">(</span><span class="n">many</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>


    <span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
        <span class="n">model</span> <span class="o">=</span> <span class="n">Album</span>
        <span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s">'album_name'</span><span class="p">,</span> <span class="s">'tracks'</span><span class="p">]</span>

<span class="n">data</span> <span class="o">=</span> <span class="n">AlbumSerializer</span><span class="p">(</span><span class="n">Album</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nb">all</span><span class="p">(),</span> <span class="n">many</span><span class="o">=</span><span class="bp">True</span><span class="p">).</span><span class="n">data</span>
</code></pre></div></div>

<p>then serializer all albums as above.</p>

<p>This cause <strong>serious performance issues</strong> since it will hit database N times with N is total number of albums.</p>

<p>It is important to address the issue of hitting the database multiple times, which can cause <strong>serious performance issues</strong>.</p>

<p>By using <code class="language-plaintext highlighter-rouge">Album.objects.prefetch_related('tracks')</code> , developers can optimize the performance by fetching the related tracks in a single database query. This reduces the number of round trips to the database and improves the overall performance of the serializer.</p>

<h2 id="relation-fields-readonly-built-in">Relation fields readonly built-in</h2>

<p>DRF provides relation fields readonly includes:</p>

<p>– <strong>StringRelatedField</strong></p>

<p>– <strong>HyperlinkedIdentityField</strong></p>

<h2 id="relation-fields-read-write-built-in">Relation fields read-write built-in</h2>

<p>DRF provides relation fields read-write includes:</p>

<p>– PrimaryKeyRelatedField</p>

<p>– HyperlinkedRelatedField</p>

<p>– SlugRelatedField</p>

<p>If you want these readonly, add param read_only=True in the field.</p>

<p>For mor detail about these built-in fields, please read the <a href="https://www.django-rest-framework.org/api-guide/relations/">official document</a>.</p>

<h2 id="nested-serializer">Nested serializer</h2>

<p>For nested relationship, you could use its own serializer. By default, nested serializer is readonly.</p>

<p>For example, Track has serializer TrackSerializer then could use:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tracks</span> <span class="o">=</span> <span class="n">TrackSerializer</span><span class="p">(</span><span class="n">many</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">read_only</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<p>Above is how to present data. If you want to write into nested relationship, you could use method create() or update() to write.</p>

<p>For example, when create an album, you also want to write tracks, then:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">validated_data</span><span class="p">):</span>
    <span class="n">tracks_data</span> <span class="o">=</span> <span class="n">validated_data</span><span class="p">.</span><span class="n">pop</span><span class="p">(</span><span class="s">'tracks'</span><span class="p">)</span>
    <span class="n">album</span> <span class="o">=</span> <span class="n">Album</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="n">create</span><span class="p">(</span><span class="o">**</span><span class="n">validated_data</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">track_data</span> <span class="ow">in</span> <span class="n">tracks_data</span><span class="p">:</span>
        <span class="n">Track</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="n">create</span><span class="p">(</span><span class="n">album</span><span class="o">=</span><span class="n">album</span><span class="p">,</span> <span class="o">**</span><span class="n">track_data</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">album</span>
</code></pre></div></div>

<h2 id="custom-relation-fields">Custom relation fields</h2>
<p>In some case, all above options not fit your needs. You could write a custom relation fields to handle.</p>

<h3 id="example-1-custom-presentation">Example 1: Custom presentation</h3>
<p>For this example, take a look on <a href="https://www.django-rest-framework.org/api-guide/relations/#example_1">document</a> where create a custom relation field named <strong>“TrackListingField”</strong> extends from <strong>“serializers.RelatedField”</strong> then override method <strong>“to_representation“</strong></p>

<h3 id="example-2-read-write-relation-fields-with-nested-serializer">Example 2: Read-write relation fields with nested serializer</h3>
<p>For this example, let’s start with this context:</p>

<p>I have class <strong>TrackSerializer</strong> as above on nested serializer, but I don’t want it just use for readonly by default, I want a <strong>read-write</strong> relation fields which could help me read and write in clean way.</p>

<p>From the guide, I will implement <strong>“.to_internal_value()”</strong> method to help it could be writable. And implement <strong>“.to_representation()”</strong> method to present the data.</p>

<p>So, it could look like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CustomRelatedField</span><span class="p">(</span><span class="n">serializers</span><span class="p">.</span><span class="n">RelatedField</span><span class="p">):</span>
    <span class="s">"""Custom Related Field for Read and Write"""</span>

    <span class="k">def</span> <span class="nf">to_representation</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
        <span class="k">pass</span>

    <span class="k">def</span> <span class="nf">to_internal_value</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">data</span><span class="p">):</span>
        <span class="k">pass</span>
</code></pre></div></div>

<h4 id="implement-to_presentation-method"><strong>Implement to_presentation method</strong></h4>

<p>As I want to use serializer class to present the data, then will need a way to get the serializer class from input of the field, then I decided to put it as a part of keyword arguments kwargs.</p>

<p>Above class could be like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">CustomRelatedField</span><span class="p">(</span><span class="n">serializers</span><span class="p">.</span><span class="n">RelatedField</span><span class="p">):</span>
    <span class="s">"""Related Field with Serializer Class for presentation"""</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">serializer_class</span> <span class="o">=</span> <span class="n">kwargs</span><span class="p">.</span><span class="n">pop</span><span class="p">(</span><span class="s">"serializer_class"</span><span class="p">)</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="p">.</span><span class="n">serializer_class</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">"serializer_class is required"</span><span class="p">)</span>

        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">to_representation</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">serializer_class</span><span class="p">(</span><span class="n">value</span><span class="p">).</span><span class="n">data</span>
</code></pre></div></div>

<p>As above, we able to present object with nested serializer class.</p>

<h4 id="implement-to_internal_value-method"><strong>Implement to_internal_value method</strong></h4>

<p>To help the relation field writable, to_internal_value must be implement. Because this method help decide the data to write to relation models.</p>

<p>There are 2 case of relationships here should be concern:</p>

<p>– ForeignKey in model stands for 1 to many relation</p>

<p>– ManyToManyField in model stands for many to many relation</p>

<p>As example of this post, the tracks belong to 1 to many relations.</p>

<p>One album able to have multiple tracks and 1 track belong to 1 album.</p>

<p><strong>1 to many relation</strong></p>

<p>As 1-n relation, I could get the album by id and set it as value to the write.</p>

<p>The to_internal_value could looks like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">to_internal_value</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">data</span><span class="p">):</span>
    <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_queryset</span><span class="p">().</span><span class="n">get</span><span class="p">(</span><span class="n">uuid</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
</code></pre></div></div>
<p>Then we have full custom relation fields for a ForeignKey field as FKRelationField below:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ForeignKeyRelationField</span><span class="p">(</span><span class="n">serializers</span><span class="p">.</span><span class="n">RelatedField</span><span class="p">):</span>
    <span class="s">"""Related Field with Serializer Class for presentation"""</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">serializer_class</span> <span class="o">=</span> <span class="n">kwargs</span><span class="p">.</span><span class="n">pop</span><span class="p">(</span><span class="s">"serializer_class"</span><span class="p">)</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">to_representation</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">serializer_class</span><span class="p">(</span><span class="n">value</span><span class="p">).</span><span class="n">data</span>

    <span class="k">def</span> <span class="nf">to_internal_value</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">data</span><span class="p">):</span>
        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_queryset</span><span class="p">().</span><span class="n">get</span><span class="p">(</span><span class="n">uuid</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
</code></pre></div></div>

<p>I changed the name class from “CustomRelatedField” to “ForeignKeyRelationField” in this case.</p>

<p><strong>many to many relation</strong></p>

<p>Many to many relation will similar to FK with minor change of internal value will be a list of ids instead of single object then each object will return an id instead.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">to_internal_value</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">data</span><span class="p">):</span>
    <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_queryset</span><span class="p">().</span><span class="n">get</span><span class="p">(</span><span class="n">uuid</span><span class="o">=</span><span class="n">data</span><span class="p">).</span><span class="nb">id</span>
</code></pre></div></div>

<p>You could named this field ManyToManyRelationField.</p>

<h2 id="using-custom-relation-field">Using custom relation field</h2>

<p>As above example, I could use my ForeignKeyRelationField on TrackSerializer to read and write the album</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">TrackSerializer</span><span class="p">(</span><span class="n">serializers</span><span class="p">.</span><span class="n">ModelSerializer</span><span class="p">):</span>
    <span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
        <span class="n">model</span> <span class="o">=</span> <span class="n">Track</span>
        <span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s">'title'</span><span class="p">,</span> <span class="s">'album'</span><span class="p">]</span>

    <span class="n">album</span> <span class="o">=</span> <span class="n">ForeignKeyRelatedField</span><span class="p">(</span>
        <span class="n">many</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
        <span class="n">queryset</span><span class="o">=</span><span class="n">Album</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nb">all</span><span class="p">(),</span>
        <span class="n">serializer_class</span><span class="o">=</span><span class="n">AlbumSerializer</span><span class="p">,</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>Above serializer could help represent album with nested serializer AlbumSerializer as well as write the album of a track.</p>

<p>And for “tracks” in AlbumSerializer, could use ManyToManyFieldRelationField to read and write for relation many to many.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AlbumSerializer</span><span class="p">(</span><span class="n">serializers</span><span class="p">.</span><span class="n">ModelSerializer</span><span class="p">):</span>

    <span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
        <span class="n">model</span> <span class="o">=</span> <span class="n">Album</span>
        <span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s">'album_name'</span><span class="p">,</span> <span class="s">'tracks'</span><span class="p">]</span>

    <span class="n">tracks</span> <span class="o">=</span> <span class="n">ManyManyKeyRelatedField</span><span class="p">(</span>
        <span class="n">many</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="n">required</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
        <span class="n">queryset</span><span class="o">=</span><span class="n">Track</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nb">all</span><span class="p">(),</span>
        <span class="n">serializer_class</span><span class="o">=</span><span class="n">TrackSerializer</span><span class="p">,</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>In this post, I have give you more detail of relational fields and how to custom a relation fields.</p>]]></content><author><name>Thanh Nguyen</name></author><category term="Django" /><category term="Django Rest Framework (DRF)" /><category term="django" /><category term="drf" /><category term="serializer" /><category term="fields" /><category term="english" /><summary type="html"><![CDATA[The Django model offers various types of relationships such as OneToOneField, ForeignKey, ManyToManyField, and GenericForeignKey.]]></summary></entry><entry><title type="html">How to define a group of constant in Django app?</title><link href="https://beautyoncode.com/django/how-to-define-a-group-of-constant-in-django-app/" rel="alternate" type="text/html" title="How to define a group of constant in Django app?" /><published>2023-05-16T00:00:00-04:00</published><updated>2023-05-16T00:00:00-04:00</updated><id>https://beautyoncode.com/django/how-to-define-a-group-of-constant-in-django-app</id><content type="html" xml:base="https://beautyoncode.com/django/how-to-define-a-group-of-constant-in-django-app/"><![CDATA[<p><img src="/assets/images/2023/05/2023-05-how-to-define-a-group-of-constant-in-django-app-cover.webp" alt="" /></p>

<h2 id="group-of-constants">Group of constants</h2>
<p>A group of constants value will help group the constant by specific type or meaning.</p>

<p>For example, this blog post might have multiple status like draft, publish then we could use a group with named <code class="language-plaintext highlighter-rouge">POST_STATUS</code> to group all of them together.</p>

<h2 id="use-cases">Use cases</h2>
<p>In a Django app, I usually use group of constants in two common use cases:</p>

<p>– Define choices for a field in the model</p>

<p>– Use this group of constants in the logic app</p>

<h2 id="how-to-define-a-group-of-constants">How to define a group of constants?</h2>
<h3 id="use-tuple">Use tuple</h3>
<p>You might define each constant type with a name then group them by a tuple, like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="n">POST_DRAFT</span><span class="p">,</span> <span class="n">POST_PUBLISHED</span><span class="p">,</span> <span class="n">POST_ARCHIVED</span><span class="p">)</span> <span class="o">=</span> <span class="nb">range</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
<span class="n">POST_STATUS_CHOICES</span> <span class="o">=</span> <span class="p">(</span>
   <span class="p">(</span><span class="n">POST_DRAFT</span><span class="p">,</span> <span class="s">'Draft'</span><span class="p">),</span>
   <span class="p">(</span><span class="n">POST_PUBLISHED</span><span class="p">,</span> <span class="s">'Published'</span><span class="p">)</span>
   <span class="p">(</span><span class="n">POST_ARCHIVED</span><span class="p">,</span> <span class="s">'Archived'</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<p>You might want to use this group of constants in a model field choices:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Post</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span>
    <span class="n">post_status</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">IntegerField</span><span class="p">(</span>
        <span class="n">choices</span><span class="o">=</span><span class="n">POST_STATUS_CHOICES</span><span class="p">,</span>
        <span class="n">default</span><span class="o">=</span><span class="n">POST_STATUS_CHOICES</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>Above code snippet also show you get the default value is <code class="language-plaintext highlighter-rouge">POST_DRAFT</code> with</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">POST_STATUS_CHOICES</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
</code></pre></div></div>

<p>This way is work but lack of reading on code, where you might forget about the value of the first one then need to re-check on where define the group.</p>

<h2 id="use-enum">Use Enum</h2>
<p>There is a better way to avoid select by the index of the value as above is using enum value.</p>

<p>Firstly, define a class stand for the choices by enums, and provide a method to get all values by tuple named “choices”</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">enum</span>
<span class="kn">from</span> <span class="nn">enum</span> <span class="kn">import</span> <span class="n">Enum</span>

<span class="o">@</span><span class="n">enum</span><span class="p">.</span><span class="n">unique</span>
<span class="k">class</span> <span class="nc">EnumChoices</span><span class="p">(</span><span class="n">Enum</span><span class="p">):</span>
    <span class="o">@</span><span class="nb">classmethod</span>
    <span class="k">def</span> <span class="nf">choices</span><span class="p">(</span><span class="n">cls</span><span class="p">):</span>
        <span class="k">return</span> <span class="p">[(</span><span class="n">item</span><span class="p">.</span><span class="n">value</span><span class="p">,</span> <span class="n">item</span><span class="p">.</span><span class="n">name</span><span class="p">)</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">cls</span><span class="p">]</span>
</code></pre></div></div>

<p>then define a class PostStatus extend from EnumChoices</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">PostStatus</span><span class="p">(</span><span class="n">EnumChoices</span><span class="p">):</span>
    <span class="n">POST_DRAFT</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">POST_PUBLISHED</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">finally</span><span class="p">,</span> <span class="n">use</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">model</span> <span class="n">field</span> <span class="n">choices</span>

<span class="k">class</span> <span class="nc">Post</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span>
    <span class="n">post_status</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">IntegerField</span><span class="p">(</span>
        <span class="n">choices</span><span class="o">=</span><span class="n">PostStatus</span><span class="p">.</span><span class="n">choices</span><span class="p">(),</span>
        <span class="n">default</span><span class="o">=</span><span class="n">PostStatus</span><span class="p">.</span><span class="n">POST_DRAFT</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>This solution help us avoid the index select by better meaningful name <code class="language-plaintext highlighter-rouge">PostStatus.POST_DRAFT</code></p>

<h3 id="use-django-choices-class">Use Django choices class</h3>
<p>Above solution is good to go, but Django makes it even better by already define enum choices class to use, like <code class="language-plaintext highlighter-rouge">IntegerChoices</code>, <code class="language-plaintext highlighter-rouge">TextChoices</code>, <code class="language-plaintext highlighter-rouge">Choices</code>.</p>

<p>Then above code will become:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">django.db.models</span> <span class="kn">import</span> <span class="n">IntegerChoices</span>
<span class="k">class</span> <span class="nc">PostStatus</span><span class="p">(</span><span class="n">IntegerChoices</span><span class="p">):</span>
    <span class="n">POST_DRAFT</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="n">POST_PUBLISHED</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">then</span> <span class="ow">in</span> <span class="n">model</span> <span class="n">field</span> <span class="n">choices</span> <span class="n">will</span> <span class="n">call</span> <span class="n">to</span> <span class="n">get</span> <span class="n">choices</span> <span class="n">value</span> <span class="n">instead</span><span class="p">:</span>

<span class="k">class</span> <span class="nc">Post</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span>
    <span class="n">post_status</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">IntegerField</span><span class="p">(</span>
        <span class="n">choices</span><span class="o">=</span><span class="n">PostStatus</span><span class="p">.</span><span class="n">choices</span><span class="p">,</span>
        <span class="n">default</span><span class="o">=</span><span class="n">PostStatus</span><span class="p">.</span><span class="n">POST_DRAFT</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>For summary, we should refer the Django choice class since it is the better option for clean code and readable constant value in the group.</p>]]></content><author><name>Thanh Nguyen</name></author><category term="Django" /><category term="django" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Các bài viết ngắn - phần 31</title><link href="https://beautyoncode.com/short%20posts/cac-bai-viet-ngan-phan-31/" rel="alternate" type="text/html" title="Các bài viết ngắn - phần 31" /><published>2023-04-05T00:00:00-04:00</published><updated>2023-04-05T00:00:00-04:00</updated><id>https://beautyoncode.com/short%20posts/cac-bai-viet-ngan-phan-31</id><content type="html" xml:base="https://beautyoncode.com/short%20posts/cac-bai-viet-ngan-phan-31/"><![CDATA[<p><img src="/assets/images/2023/04/2023-04-05-cac-bai-viet-ngan-phan-31-1.webp" alt="" /></p>

<h2 id="blog-từ-tiết-kiệm-đến-miễn-phí">Blog từ tiết kiệm đến miễn phí</h2>
<p>Chia sẻ kinh nghiệm viết blog từ tiết kiệm đến miễn phí</p>

<p>Khi bắt đầu tập viết blog cá nhân, bạn sẽ cần có nơi host trang web và domain riêng của trang, hoặc bạn sẽ được giới thiệu, hướng dẫn nên làm như thế ^^</p>

<p>Blog beautyoncode.com hiện tại đang sử dụng wordpress, host trên stablehost và domain godaddy. Mỗi năm tốn khoảng hơn 1 triệu để duy trì (thuê domain hàng năm và thuê host 3 năm).</p>

<p>Khi mới bắt đầu, mình chọn wordpress xây dựng sẵn vì nó dễ sử dụng nhất, mình dùng wordpress plugin elementor để xây dựng trang web từ các khối kéo thả và viết nội dung là công việc chính.</p>

<p>Sau một thời gian sử dụng, có một số bất tiện với dev như mình:
– bài viết lập trình thường có nhiều code ví dụ, nên chuyển đổi code sang html thì mới nhúng vào trang web được. Mình dùng tool online tohtml.com để chuyển.
– khi chuyển nội dung blog qua viblo hay kipalog, mình cần copy và chuyển qua markdown rồi đăng.
– trang blog khá chậm</p>

<p>Gần đây, mình tập viết blog tiếng anh, nên muốn chuyển nội dung của blog tiếng Việt beautyoncode.com sang domain mới là beautyoncode.online, luôn tiện cải thiện các bất tiện trên.</p>

<p>Github Page với jekyll giúp mình một số điểm:
– github page giúp tiết kiệm khoản tiền bảo trì website (domain + host). Nếu bạn muốn có domain khác ngoài account.githubio.com thì có thể cài đặt là “Pages” và thêm DNS ở nhà cung cấp domain như godaddy hay route53 để có thể chuyển sang.
– thân thiện với dev: mình có thể viết blog với markdown và quản lý source code, hay chạy web ở local.</p>

<p>Tuy nhiên, github page sẽ chưa phù hợp nếu bạn:
– hoàn toàn không biết gì về lập trình
– muốn xây dựng trang blog sử dụng nhiều tính năng như bên wordpress (admin, category, plugin, comment)
– không muốn public source của mình</p>

<p>Nếu bạn đã có dự định viết gì đó lại hoàn toàn miễn phí thì lựa chọn này rất đáng cân nhắc nha.
Ghé thăm trang thử nghiệm mình mới làm ở đây
https://beautyoncode.com/</p>

<h2 id="xuất-file-docx-trong-django">Xuất file docx trong Django</h2>
<p>Việc xuất các loại file là một tính năng thường được sử dụng cho phép người dùng lấy lại dữ liệu của mình.</p>

<p>Trong series này, mình sẽ giới thiệu các phương pháp khác nhau để xuất tập tin trong ứng dụng Django. Các định dạng tệp được xuất có thể bao gồm docx, csv, zip hoặc pdf.</p>

<p>Trong bài viết đầu tiên, mình sẽ giới thiệu quá trình xuất tập tin docx, bằng cách sử dụng một thư viện gọi là python-docx.</p>

<p>python-docx là một thư viện Python cho phép tạo và cập nhật các tập tin Microsoft Word (.docx).</p>

<p>Về cơ bản, python-docx là tạo một đối tượng document và bạn có thể thêm nội dung như đoạn văn bản, tiêu đề, ngắt trang, bảng, hình ảnh và các tùy chọn định dạng như đậm hoặc nghiêng.</p>

<p>Bên cạnh việc xây dựng một docx file cơ bản, bài viết giới thiệu đến bạn cách xử lý nội dung HTML để giữ định dạng mong muốn bằng cách viết class custom HTMLParser để xử lý.</p>

<p>Cuối cùng, việc viết unit test cho code là quan trọng để đảm bảo chất lượng code đúng, mình giới thiệu unit test gồm hai phần: test view response và test nội dung document.</p>

<p>Bạn ghé đọc nội dung <a href="/django/export-docx-in-django/">bài viết này</a> ở blog nha, có code ví dụ và các hình ảnh minh hoạt rất chi tiết.</p>

<h2 id="tìm-hiểu-về-dns">Tìm hiểu về DNS</h2>
<p>Người dùng sử dụng web thông qua các tên miền (domain) của trang web như beautyoncode.com. Các trình duyệt web thì lại tương tác bằng địa chỉ IP.</p>

<p>DNS (Domain Name System) là hệ thống đứng trung gian để dịch từ tên miền ở ngôn ngữ tự nhiên như google.com thành địa chỉ IP như 172.168.23.14</p>

<p>DNS là xương sống của internet. DNS sử dụng cấu trúc phân cấp theo tên</p>

<p>Các thuật ngữ về DNS: Domain registrar, DNS records, Zone file, Name server, Top level domain (TLD), Second level domain (SLD)</p>

<p>Bản ghi DNS (DNS record)</p>

<p>Mỗi bản ghi bao gồm: tên Domain hoặc sub domain, loại bản ghi, giá trị (value), chính sách định tuyến (routing policy)</p>

<p>Record type A, CNAME và alias record</p>

<p>– Record type A gắn với địa chỉ IPv4 cụ thể.</p>

<p>Ví dụ: khi bạn muốn domain của mình gắn đến địa chỉ IP của github page là 172.168.76.89 thì sẽ cần tạo một record loại A</p>

<p>– Record type CNAME giúp gắn domain của bạn với đến domain khác, và domain này cần phải có địa chỉ IP hay record A gắn vào nó.</p>

<p>Không thể tạo CNAME cho SLD domain (apex domain) như beautyoncode.com. Nhưng có thể tạo CNAME record cho sub domain như www.beautyoncode.com</p>

<p>Ví dụ: sau khi đã gắn IP của github vào record A ở trên, bạn tạo thêm một record loại CNAME để chuyển truy cập từ www.beautyoncode.com sang beautyoncode.com.</p>

<p>Khi đó, trang web của bạn đã chuyển từ account.github.io sang domain.com thành công (lưu ý TTL để kiểm tra lại, có thể là 24h)</p>

<p>– Alias giúp gắn domain của bạn đến các nguồn tài nguyên khác như AWS ALB, và có thể sử dụng với apex domain. Loại record thường sử dụng cho alias là A và không gán TTL.</p>

<p>DNS hoạt động như thế nào? (mời bạn xem sơ đồ minh hoạ trong <a href="/network/tim-hieu-ve-dns/">bài viết ở blog</a> nhé)</p>

<h2 id="learning-by-doing-với-exercismorg">Learning by doing với <a href="https://exercism.org/">exercism.org</a></h2>
<p>Exercism hỗ trợ học với hơn 67 ngôn ngữ lập trình thông qua các bài học coding thực hành.</p>

<p>Nền tảng này phù hợp với những người học lập trình với nhiều trình độ khác nhau (junior, mid-level, senior).</p>

<p>Thử tưởng tượng bạn sẽ giải các bài tập coding, rồi xem các cách giải khác từ những người khác, và có cả mentor hướng dẫn bạn các điểm giúp code tốt hơn.</p>

<p>Mỗi ngôn ngữ sẽ có một track gồm các bài tập theo chủ để cơ bản của ngôn ngữ (các concepts). Ví dụ Python có 15 concepts và 137 exercies.</p>

<p>Để giải bài tập, bạn có thể download tools về máy tính riêng hay giải trực tiếp trên nền tảng.</p>

<p>Bạn cũng có thể thử với vai trò mentor cho những người khác để giúp cộng đồng ngày càng lớn mạnh.</p>

<p>Nền tảng này có giao diện cực đẹp nha, enjoy your learning!</p>

<h2 id="liệu-ngày-tàn-frontend-đã-đến">Liệu ngày tàn frontend đã đến?</h2>
<p>Chủ đề về chat GPT được mọi người bàn luận sôi nổi trên tất cả các kênh. Mình đã chọn im lặng và mua thêm vào con bot để xài (ChatGPT, Copilot) rồi học dần về AI ^^</p>

<p>Hôm nay đọc bài viết của tác giả mình yêu thích bàn chủ đề này nên chia sẻ cùng cả nhà một vài gạch đầu dòng của bài viết:</p>

<p>Chuyện đe doạ developer mất việc đã xảy ra trước đây, khi mà các website được xây dựng no-code (WordPress, Webflow, tools) được ưa chuộng chỉ với vài đô mỗi tháng thay vì thuê lập trình viên làm từ đầu.
Thế nhưng lập trình viên vẫn tồn tại, và còn phát triển hơn nữa.</p>

<p>Gần đây, OpenAI ra mắt Chat-GPT4 với demo khá ấn tượng khi với bản viết tay vài dòng ChatGPT có thể dựng một trang web.
Tuy nhiên, lập trình viên ngày nay làm các trang web phức tạp hơn vậy nhiều.</p>

<p>Thêm nữa, các kết quả của ChatGPT thực tế có độ chính xác tầm 80%. Tương lai độ chính xác sẽ ngày càng cao, nhưng ai là người xác định kết quả đầu ra có chính xác hay không? Đó chính là lập trình viên.</p>

<p>Cứ cho là AI có thể giúp xây dựng một trang web lớn với vài chục nghìn dòng code, nhưng khi có lỗi xảy ra ai sẽ là người sửa và đảm bảo nó vẫn hoạt động. Các developer sẽ là người hiểu code của dự án và đảm bảo kết quả cuối cùng đến tay người dùng.</p>

<p>AI là công cụ hỗ trợ, không phải thay thế
Một câu nói mà boss mình mới nhắn gần đây:
“AI will not replace humans but humans with AI will replace humans without AI.”
AI sẽ giúp nâng hiệu suất công việc lên, và sẽ càng có nhiều công việc hơn để làm.</p>

<p>Chat-GPT giúp bạn học và làm nhanh hơn. Nhưng lưu ý cần kiểm tra đầu ra cẩn thận, hỏi thêm câu hỏi đến giải thích các phần liên quan, không copy code mù quáng, chắc chắn bạn hiểu rồi mới sử dụng và kiểm tra kết quả đầu ra.</p>

<p>Đọc thêm ở <a href="https://www.joshwcomeau.com/blog/the-end-of-frontend-development/">đây</a></p>]]></content><author><name>Thanh Nguyen</name></author><category term="Short Posts" /><category term="short-posts" /><category term="django" /><category term="blog" /><category term="devops" /><category term="learning-resources" /><category term="frontend" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Export multiple CSV files into a ZIP in Django Application</title><link href="https://beautyoncode.com/django/export-multiple-csv-to-zip-django/" rel="alternate" type="text/html" title="Export multiple CSV files into a ZIP in Django Application" /><published>2023-04-01T00:00:00-04:00</published><updated>2023-04-01T00:00:00-04:00</updated><id>https://beautyoncode.com/django/export-multiple-csv-to-zip-django</id><content type="html" xml:base="https://beautyoncode.com/django/export-multiple-csv-to-zip-django/"><![CDATA[<p><img src="/assets/images/2023/04/2023-04-export-multiple-csv-to-zip-django-cover.png" alt="" /></p>

<p>In this next installment of the Django export series, I will be demonstrating how to create a <code class="language-plaintext highlighter-rouge">zip</code> file containing multiple CSV files. Throughout the post, we will explore various methods, providing you with a range of options to consider for your own project.</p>

<h2 id="models-example">Models example</h2>
<p>Let’s consider a basic library system in which a book can be associated with multiple libraries.</p>

<p>The corresponding models are structured as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Library</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span>
    <span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">TextField</span><span class="p">()</span>


<span class="k">class</span> <span class="nc">Book</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span>
    <span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">TextField</span><span class="p">()</span>
    <span class="n">libraries</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="n">ManyToManyField</span><span class="p">(</span>
        <span class="n">null</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="n">to</span><span class="o">=</span><span class="s">'Library'</span><span class="p">,</span>
        <span class="n">related_name</span><span class="o">=</span><span class="s">'books'</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>Our objective is to export a zip file that contains several CSV files, each one representing a library and displaying a list of books available in that library.</p>

<h3 id="export-view">Export view</h3>

<p>As is typical when creating a download API, we will create a view that only allows the GET method.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">csv</span><span class="p">,</span> <span class="n">io</span><span class="p">,</span> <span class="n">zipfile</span>
<span class="kn">from</span> <span class="nn">wsgiref.util</span> <span class="kn">import</span> <span class="n">FileWrapper</span>
<span class="kn">from</span> <span class="nn">django.http</span> <span class="kn">import</span> <span class="n">StreamingHttpResponse</span>
<span class="kn">from</span> <span class="nn">rest_framework.views</span> <span class="kn">import</span> <span class="n">APIView</span>

<span class="k">class</span> <span class="nc">ExportZip</span><span class="p">(</span><span class="n">APIView</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">csv_datas</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">build_multiple_csv_files</span><span class="p">()</span>

        <span class="n">temp_file</span> <span class="o">=</span> <span class="n">io</span><span class="p">.</span><span class="n">BytesIO</span><span class="p">()</span>
        <span class="k">with</span> <span class="n">zipfile</span><span class="p">.</span><span class="n">ZipFile</span><span class="p">(</span>
             <span class="n">temp_file</span><span class="p">,</span> <span class="s">"w"</span><span class="p">,</span> <span class="n">zipfile</span><span class="p">.</span><span class="n">ZIP_DEFLATED</span>
        <span class="p">)</span> <span class="k">as</span> <span class="n">temp_file_opened</span><span class="p">:</span>
            <span class="c1"># add csv files each library
</span>            <span class="k">for</span> <span class="n">data</span> <span class="ow">in</span> <span class="n">csv_datas</span><span class="p">:</span>
                <span class="n">data</span><span class="p">[</span><span class="s">"csv_file"</span><span class="p">].</span><span class="n">seek</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
                <span class="n">temp_file_opened</span><span class="p">.</span><span class="n">writestr</span><span class="p">(</span>
                    <span class="sa">f</span><span class="s">"library_</span><span class="si">{</span><span class="n">data</span><span class="p">[</span><span class="s">'library_name'</span><span class="p">]</span><span class="si">}</span><span class="s">.csv"</span><span class="p">,</span>
                    <span class="n">data</span><span class="p">[</span><span class="s">"csv_file"</span><span class="p">].</span><span class="n">getvalue</span><span class="p">()</span>
                <span class="p">)</span>

        <span class="n">temp_file</span><span class="p">.</span><span class="n">seek</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

        <span class="c1"># put them to streaming content response
</span>        <span class="c1"># within zip content_type
</span>        <span class="n">response</span> <span class="o">=</span> <span class="n">StreamingHttpResponse</span><span class="p">(</span>
            <span class="n">FileWrapper</span><span class="p">(</span><span class="n">temp_file</span><span class="p">),</span>
            <span class="n">content_type</span><span class="o">=</span><span class="s">"application/zip"</span><span class="p">,</span>
        <span class="p">)</span>

        <span class="n">response</span><span class="p">[</span><span class="s">'Content-Disposition'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'attachment;filename=Libraries.zip'</span>
        <span class="k">return</span> <span class="n">response</span>

    <span class="k">def</span> <span class="nf">build_multiple_csv_files</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">csv_files</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">return</span> <span class="n">csv_files</span>
</code></pre></div></div>

<p>In the aforementioned view, we utilize the Python Standard Library’s <a href="https://docs.python.org/3/library/zipfile.html"><code class="language-plaintext highlighter-rouge">zipfile</code> module</a> for compressing and archiving data.</p>

<p>The <a href="https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile"><code class="language-plaintext highlighter-rouge">zipfile.ZipFile</code></a> method enables us to open a zip file for writing. In this instance, the file is a binary I/O object, specified as <code class="language-plaintext highlighter-rouge">temp_file_opened</code>, with the <code class="language-plaintext highlighter-rouge">temp_file</code> object being its <a href="https://docs.python.org/3/library/io.html#module-io">file-like</a> equivalent.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">zipfile</span><span class="p">.</span><span class="n">ZipFile</span><span class="p">(</span>
    <span class="nb">file</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">'r'</span><span class="p">,</span> <span class="n">compression</span><span class="o">=</span><span class="n">ZIP_STORED</span><span class="p">,</span>
    <span class="n">allowZip64</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">compresslevel</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>
    <span class="o">*</span><span class="p">,</span> <span class="n">strict_timestamps</span><span class="o">=</span><span class="bp">True</span>
<span class="p">)</span>
</code></pre></div></div>

<p>We utilize a context manager via the <code class="language-plaintext highlighter-rouge">"with"</code> statement to guarantee the closure of our zip file after the suite within the “with” block has been executed, even if an exception is raised.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">temp_file</span> <span class="o">=</span> <span class="n">io</span><span class="p">.</span><span class="n">BytesIO</span><span class="p">()</span>
<span class="k">with</span> <span class="n">zipfile</span><span class="p">.</span><span class="n">ZipFile</span><span class="p">(</span>
    <span class="n">temp_file</span><span class="p">,</span> <span class="s">"w"</span><span class="p">,</span> <span class="n">zipfile</span><span class="p">.</span><span class="n">ZIP_DEFLATED</span>
<span class="p">)</span> <span class="k">as</span> <span class="n">temp_file_opened</span><span class="p">:</span>
    <span class="c1"># write to zip file
</span></code></pre></div></div>

<p>Within the context manager, we write the CSV content file to the zip <code class="language-plaintext highlighter-rouge">temp_file_opened</code> using the <code class="language-plaintext highlighter-rouge">writestr</code> method.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ZipFile</span><span class="p">.</span><span class="n">writestr</span><span class="p">(</span>
    <span class="n">zinfo_or_arcname</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span>
    <span class="n">compress_type</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">compresslevel</span><span class="o">=</span><span class="bp">None</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Here, we specify two mandatory parameters - <code class="language-plaintext highlighter-rouge">zinfo_or_arcname</code> and <code class="language-plaintext highlighter-rouge">data</code>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">temp_file_opened</span><span class="p">.</span><span class="n">writestr</span><span class="p">(</span>
    <span class="sa">f</span><span class="s">"File_library_</span><span class="si">{</span><span class="nb">file</span><span class="p">[</span><span class="s">'lib'</span><span class="p">]</span><span class="si">}</span><span class="s">.csv"</span><span class="p">,</span>
    <span class="nb">file</span><span class="p">[</span><span class="s">"csv_file"</span><span class="p">].</span><span class="n">getvalue</span><span class="p">()</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Once we have completed writing multiple CSV files, we locate the zip file by using <a href="https://docs.python.org/3/library/io.html#io.IOBase.seek">seek</a>. We then convert the file-like objects to an iterator using <a href="http://filewrapper/">FileWrapper</a> before returning them in the StreamingHttpResponse.</p>

<p>At this stage, we can download an empty file named “Libraries.zip”.</p>

<h2 id="build-csv-files">Build CSV files</h2>

<p>As shown, we have defined a method named <code class="language-plaintext highlighter-rouge">"build_multiple_csv_files"</code> that currently returns an empty list. In the following step, we will add the code to this function to generate a list of CSV files.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ExportLibraries</span><span class="p">(</span><span class="n">APIView</span><span class="p">):</span>
    <span class="n">header_data</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">"name"</span><span class="p">:</span> <span class="s">"Name"</span><span class="p">,</span>
        <span class="s">"library"</span><span class="p">:</span> <span class="s">"Library Name"</span>
    <span class="p">}</span>

    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="p">...</span>
        <span class="k">return</span> <span class="n">response</span>

    <span class="k">def</span> <span class="nf">build_multiple_csv_files</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">libraries</span><span class="p">,</span> <span class="n">books</span><span class="p">):</span>
        <span class="n">csv_files</span> <span class="o">=</span> <span class="p">[]</span>

        <span class="k">for</span> <span class="n">library</span> <span class="ow">in</span> <span class="n">libraries</span><span class="p">.</span><span class="n">iterator</span><span class="p">():</span>
            <span class="n">mem_file</span> <span class="o">=</span> <span class="n">io</span><span class="p">.</span><span class="n">StringIO</span><span class="p">()</span>
            <span class="n">writer</span> <span class="o">=</span> <span class="n">csv</span><span class="p">.</span><span class="n">DictWriter</span><span class="p">(</span>
                <span class="n">mem_file</span><span class="p">,</span> <span class="n">fieldnames</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">header_data</span><span class="p">.</span><span class="n">keys</span><span class="p">()</span>
            <span class="p">)</span>
            <span class="n">writer</span><span class="p">.</span><span class="n">writerow</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">header_data</span><span class="p">)</span>

            <span class="n">books_in_library</span> <span class="o">=</span> <span class="n">books</span><span class="p">.</span><span class="nb">filter</span><span class="p">(</span><span class="n">libraries__in</span><span class="o">=</span><span class="p">[</span><span class="n">library</span><span class="p">.</span><span class="nb">id</span><span class="p">])</span>
            <span class="k">for</span> <span class="n">book</span> <span class="ow">in</span> <span class="n">books_in_library</span><span class="p">:</span>
                <span class="n">book_row</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">build_book_row</span><span class="p">(</span><span class="n">book</span><span class="p">,</span> <span class="n">library</span><span class="p">)</span>
                <span class="n">writer</span><span class="p">.</span><span class="n">writerow</span><span class="p">(</span><span class="n">book_row</span><span class="p">)</span>

            <span class="n">mem_file</span><span class="p">.</span><span class="n">seek</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

            <span class="n">csv_files</span><span class="p">.</span><span class="n">append</span><span class="p">({</span>
                <span class="s">"library_name"</span><span class="p">:</span> <span class="n">library</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
                <span class="s">"csv_file"</span><span class="p">:</span> <span class="n">mem_file</span>
            <span class="p">})</span>

        <span class="k">return</span> <span class="n">csv_files</span>

    <span class="k">def</span> <span class="nf">build_book_row</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">book</span><span class="p">,</span> <span class="n">library</span><span class="p">):</span>
        <span class="n">row</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">header_data</span><span class="p">.</span><span class="n">copy</span><span class="p">()</span>

        <span class="n">row</span><span class="p">[</span><span class="s">"name"</span><span class="p">]</span> <span class="o">=</span> <span class="n">book</span><span class="p">.</span><span class="n">name</span>
        <span class="n">row</span><span class="p">[</span><span class="s">"library"</span><span class="p">]</span> <span class="o">=</span> <span class="n">library</span><span class="p">.</span><span class="n">name</span>

        <span class="k">return</span> <span class="n">row</span>
</code></pre></div></div>

<p>Reviewing the code above, we iterate over all libraries and construct a CSV file for each one. This is achieved by initializing a writer object using <code class="language-plaintext highlighter-rouge">csv.DictWriter()</code> and the keys from <code class="language-plaintext highlighter-rouge">header_data</code>.</p>

<p>Here is an example of what <code class="language-plaintext highlighter-rouge">header_data</code> might look like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">header_data</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"name"</span><span class="p">:</span> <span class="s">"Book Name"</span><span class="p">,</span>
    <span class="s">"library"</span><span class="p">:</span> <span class="s">"Library Name"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Next, we add the header to the writer and utilize a loop to add each book row by row to the writer using the .<code class="language-plaintext highlighter-rouge">write_row()</code> method.</p>

<p>Once the writing is complete, we append an object for each library, with the library’s name included in the object’s name, to help establish the CSV filename along with the file’s contents.</p>

<p>To enable export functionality, you may add this view to the URLs file.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
    <span class="n">path</span><span class="p">(</span>
        <span class="s">'export_libraries/'</span><span class="p">,</span>
        <span class="n">ExportLibraries</span><span class="p">.</span><span class="n">as_view</span><span class="p">(),</span>
        <span class="n">name</span><span class="o">=</span><span class="s">"export_libraries"</span>
    <span class="p">)</span>
<span class="p">]</span>
</code></pre></div></div>

<h2 id="unit-test">Unit Test</h2>

<p>The following question is how to verify the output?</p>

<p>It is recommended to carry out this step before implementing the logic, as per the <code class="language-plaintext highlighter-rouge">TDD</code> (Test Driven Development) approach. However, I will demonstrate how the logic works first, as it may help you better understand which components require testing.</p>

<p>I intend to create two unit tests for this:</p>

<p>One for the API: Upon calling the API, a zip file should be exported.
One for the CSV files and their contents: Calling <code class="language-plaintext highlighter-rouge">build_multiple_csv_files()</code> on the view should return a list containing data for each library. At this stage, the content of each CSV file can also be verified row by row.</p>

<p><strong>NOTE</strong>: Please keep in mind that the sample unit tests provided below are solely intended to provide an idea of what they may look like, and you should adapt them according to your specific feature.</p>

<h3 id="test-content-response">Test content response</h3>

<p>This unit test is straightforward; we only need to verify that the API call returns a <code class="language-plaintext highlighter-rouge">200</code> status code and that the exported file is a zip file with the expected name.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_export_libraries</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s">'export_libraries'</span><span class="p">))</span>
    <span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="ow">is</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_200_OK</span>
    <span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'Content-Disposition'</span><span class="p">)</span> <span class="o">==</span> <span class="s">"Libraries.zip"</span>
</code></pre></div></div>

<h3 id="test-view-function-to-get-multiple-csv-files">Test view function to get multiple CSV files</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_build_csvs_files</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="c1"># assume we mock 2 libraries
</span>    <span class="c1"># library_1, library_2
</span>    <span class="c1"># queryset is books and libraries
</span>    <span class="n">view</span> <span class="o">=</span> <span class="n">ExportRecipesCost</span><span class="p">()</span>
    <span class="n">view</span><span class="p">.</span><span class="n">request</span> <span class="o">=</span> <span class="n">drf_request_for_context</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">user</span><span class="p">)</span>
    <span class="n">csv_files</span> <span class="o">=</span> <span class="n">view</span><span class="p">.</span><span class="n">build_multiple_csv_files</span><span class="p">(</span>
        <span class="n">libraries</span><span class="p">,</span> <span class="n">books</span>
    <span class="p">)</span>
    <span class="c1"># check number of csv files
</span>    <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">csv_files</span><span class="p">)</span> <span class="o">==</span> <span class="mi">2</span>
    <span class="c1"># first csv file
</span>    <span class="k">assert</span> <span class="n">csv_files</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="s">"library_name"</span><span class="p">]</span> <span class="o">==</span> <span class="n">library_1</span><span class="p">.</span><span class="n">name</span>
    <span class="k">assert</span> <span class="n">csv_files</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="s">"csv_file"</span><span class="p">]</span>
    <span class="c1"># go check csv content in first file here
</span>
    <span class="c1"># second csv file
</span>    <span class="k">assert</span> <span class="n">csv_files</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="s">"library_name"</span><span class="p">]</span> <span class="o">==</span> <span class="n">library_2</span><span class="p">.</span><span class="n">name</span>
    <span class="k">assert</span> <span class="n">csv_files</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="s">"csv_file"</span><span class="p">]</span>
    <span class="c1"># go check csv content in first file here
</span></code></pre></div></div>

<p>I have added some comments in the code to indicate the data we will use for testing. Our approach is to call the view function using a mock DRF request, created using the <code class="language-plaintext highlighter-rouge">drf_request_for_context</code> utility function.</p>

<p>In addition to checking the number of CSV files returned, we can also verify the content of each CSV file based on its header.</p>

<h2 id="optimizing-performance">Optimizing performance</h2>

<p>To improve performance, we can optimize the current solution by using a single loop to prepare all the books data, regardless of the library it belongs to, and then loop through the libraries using this set of data. This approach can significantly reduce processing time.</p>

<p>Instead of solely relying on Django queries, one could use Pandas to flatten the data and make the export process even easier by working with dataframes.</p>

<p>Another improvement could be to handle the export process as a background task, such as a Celery task. Instead of using StreamingHttpResponse to download the file from the browser, we can upload the zip file to a service like S3 and provide the user with a URL or other means of accessing the file. This approach can improve user experience and prevent timeout errors when handling large amounts of data.</p>

<p>(If you have any other ideas for improving performance, please share them with me.)</p>

<h2 id="final-word">Final word</h2>

<p>To summarize, we have explored a method (referred to as “use Django queries”) for exporting a zip file containing multiple CSV files within a Django application. While there is an alternative approach using pandas to export data from a Django app, we may discuss it in the future.</p>]]></content><author><name>Thanh Nguyen</name></author><category term="Django" /><category term="django" /><category term="export" /><category term="csv" /><category term="zip" /><category term="english" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Export a docx file in Django application</title><link href="https://beautyoncode.com/django/export-docx-in-django/" rel="alternate" type="text/html" title="Export a docx file in Django application" /><published>2023-03-25T00:00:00-04:00</published><updated>2023-03-25T00:00:00-04:00</updated><id>https://beautyoncode.com/django/export-docx-in-django</id><content type="html" xml:base="https://beautyoncode.com/django/export-docx-in-django/"><![CDATA[<p><img src="/assets/images/2023/03/2023-03-export-docx-in-django-cover.png" alt="" /></p>

<p>Exporting file is a commonly used feature that allows users to retrieve their data.</p>

<p>Throughout this series, I will outline various methods for exporting files in a Django application. The exported file formats may include <code class="language-plaintext highlighter-rouge">docx</code>, <code class="language-plaintext highlighter-rouge">csv</code>, <code class="language-plaintext highlighter-rouge">zip</code>, or <code class="language-plaintext highlighter-rouge">pdf</code>.</p>

<p>In this initial post of the series, I will introduce the process of exporting a <code class="language-plaintext highlighter-rouge">docx</code> file. We will be utilizing a library called <code class="language-plaintext highlighter-rouge">python-docx</code> to achieve this.</p>

<h2 id="python-docx">python-docx</h2>
<h3 id="introduction">Introduction</h3>
<p><code class="language-plaintext highlighter-rouge">python-dox</code> is a Python library for creating and updating Microsoft Word (<code class="language-plaintext highlighter-rouge">.docx</code>) files.</p>

<p>Please checkout the <a href="https://python-docx.readthedocs.io/en/latest/">official documentation</a> here.</p>

<p>The fundamental concept behind <code class="language-plaintext highlighter-rouge">python-docx</code> is to create a document object to which you can add content such as paragraphs, headings, page breaks, tables, pictures and styling options like bold or italic.</p>

<p>Example:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">docx</span> <span class="kn">import</span> <span class="n">Document</span>
<span class="n">document</span> <span class="o">=</span> <span class="n">Document</span><span class="p">()</span>
<span class="n">document</span><span class="p">.</span><span class="n">add_paragraph</span><span class="p">(</span><span class="s">'Lorem ipsum dolor sit amet.'</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="installing">Installing</h3>
<p>To install <a href="https://pypi.org/project/python-docx/"><code class="language-plaintext highlighter-rouge">python-docx</code></a>, run command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>python-docx
</code></pre></div></div>

<h3 id="export-view">Export view</h3>

<p>To enable the download of a docx file through an API, we typically create a view that allows only the GET method.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ExportDocx</span><span class="p">(</span><span class="n">APIView</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
        <span class="c1"># create an empty document object
</span>        <span class="n">document</span> <span class="o">=</span> <span class="n">Document</span><span class="p">()</span>

        <span class="c1"># save document info
</span>        <span class="nb">buffer</span> <span class="o">=</span> <span class="n">io</span><span class="p">.</span><span class="n">BytesIO</span><span class="p">()</span>
        <span class="c1"># save your memory stream
</span>        <span class="n">document</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="nb">buffer</span><span class="p">)</span>
        <span class="c1"># rewind the stream to a file
</span>        <span class="nb">buffer</span><span class="p">.</span><span class="n">seek</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

        <span class="c1"># put them to streaming content response
</span>        <span class="c1"># within docx content_type
</span>        <span class="n">response</span> <span class="o">=</span> <span class="n">StreamingHttpResponse</span><span class="p">(</span>
            <span class="c1"># use the stream's content
</span>            <span class="n">streaming_content</span><span class="o">=</span><span class="nb">buffer</span><span class="p">,</span>
            <span class="n">content_type</span><span class="o">=</span><span class="s">'application/vnd.openxmlformats-officedocument.wordprocessingm'</span>
        <span class="p">)</span>

        <span class="n">response</span><span class="p">[</span><span class="s">'Content-Disposition'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'attachment;filename=Test.docx'</span>
        <span class="n">response</span><span class="p">[</span><span class="s">"Content-Encoding"</span><span class="p">]</span> <span class="o">=</span> <span class="s">'UTF-8'</span>

        <span class="k">return</span> <span class="n">response</span>
</code></pre></div></div>

<p>Once we have created an empty document, the next step is to save it and send it to the response.</p>

<p><code class="language-plaintext highlighter-rouge">python-docx</code> provides a <code class="language-plaintext highlighter-rouge">document.save()</code> method that acceps a stream instead of a file name.</p>

<p>We can initialize an <code class="language-plaintext highlighter-rouge">io.BytesIO()</code> object to store the document information and then send that to the user.</p>

<p>To handle large data and return a response, we use the <code class="language-plaintext highlighter-rouge">StreamingHttpResponse</code> function and set the content type to <code class="language-plaintext highlighter-rouge">application/vnd.openxmlformats-officedocument.wordprocessingm</code> for docx files.</p>

<h2 id="build-document-content">Build document content</h2>

<p>After enable to download an empty docx file, the next step is to begin building the content for the docx. It is recommended to refer to the <code class="language-plaintext highlighter-rouge">python-docx</code> documentation for detailed instructions.</p>

<p>To add header text, you can use the <code class="language-plaintext highlighter-rouge">.add_heading()</code> method, and to add paragraphs, you can use the <code class="language-plaintext highlighter-rouge">.add_paragraph()</code> method.</p>

<p>If you wish to style the text, you can add a run to a paragraph.</p>

<p>As an example, I have created a <code class="language-plaintext highlighter-rouge">build_document()</code> method which builds all the content in the document.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">build_document</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="n">document</span> <span class="o">=</span> <span class="n">Document</span><span class="p">()</span>

    <span class="c1"># add a header
</span>    <span class="n">document</span><span class="p">.</span><span class="n">add_heading</span><span class="p">(</span><span class="s">"This is a header"</span><span class="p">)</span>

    <span class="c1"># add a paragraph
</span>    <span class="n">document</span><span class="p">.</span><span class="n">add_paragraph</span><span class="p">(</span><span class="s">"This is a normal style paragraph"</span><span class="p">)</span>

    <span class="c1"># add a paragraph within an italic text then go on with a break.
</span>    <span class="n">paragraph</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="n">add_paragraph</span><span class="p">()</span>
    <span class="n">run</span> <span class="o">=</span> <span class="n">paragraph</span><span class="p">.</span><span class="n">add_run</span><span class="p">()</span>
    <span class="n">run</span><span class="p">.</span><span class="n">italic</span> <span class="o">=</span> <span class="bp">True</span>
    <span class="n">run</span><span class="p">.</span><span class="n">add_text</span><span class="p">(</span><span class="s">"text will have italic style"</span><span class="p">)</span>
    <span class="n">run</span><span class="p">.</span><span class="n">add_break</span><span class="p">()</span>

    <span class="k">return</span> <span class="n">document</span>
</code></pre></div></div>

<p>I will then replace the code that creates an empty document in the view with the following:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">document</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">build_document</span><span class="p">()</span>
</code></pre></div></div>

<p>The current export result is shown below:</p>

<p><img src="/assets/images/2023/03/2023-03-export-docx-in-django-img-1-basic.webp" alt="" /></p>

<h2 id="advance---build-html-content">Advance - build html content</h2>

<p>Essentially, I can export a docx file with content in it.</p>

<p>To begin with, I simply add the content within a paragraph:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># add paragraph for html content
</span><span class="n">document</span><span class="p">.</span><span class="n">add_paragraph</span><span class="p">(</span><span class="s">"&lt;p&gt;Nice to see Prep note 2&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Prep note 2 content 1&lt;/li&gt;&lt;li&gt;Prep note 2 content 2&lt;/li&gt;&lt;/ul&gt;"</span><span class="p">)</span>
</code></pre></div></div>

<p>However, there was a strange display as following:</p>

<p><img src="/assets/images/2023/03/2023-03-export-docx-in-django-img-2-issue-html.webp" alt="" /></p>

<p>I need to find a way to convert HTML content to plain text while preserving basic formatting such as italics, bolding, and bullet points. Here’s an example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Nice to see Prep note 2
    ●    Prep note 2 content 1
    ●    Prep note 2 content 2
</code></pre></div></div>

<p>After research around, I discovered a Python built-in library called <a href="https://docs.python.org/3/library/html.parser.html"><code class="language-plaintext highlighter-rouge">html.parser</code></a> - Simple HTML and XHTML parser.</p>

<p>Followed <a href="https://github.com/python-openxml/python-docx/issues/352">an examle</a> to create a class called <code class="language-plaintext highlighter-rouge">DocumentHTMLParser</code> to handle it, as shown below:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DocumentHTMLParser</span><span class="p">(</span><span class="n">HTMLParser</span><span class="p">):</span>
    <span class="s">"""
    Document Within HTML Parser
    """</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">document</span><span class="p">):</span>
        <span class="s">"""
        Override __init__ method
        """</span>
        <span class="n">HTMLParser</span><span class="p">.</span><span class="n">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">document</span> <span class="o">=</span> <span class="n">document</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">paragraph</span> <span class="o">=</span> <span class="bp">None</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">run</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">def</span> <span class="nf">add_paragraph_and_feed</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">html</span><span class="p">):</span>
        <span class="s">"""
        Custom method where add paragraph and feed
        """</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">paragraph</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">document</span><span class="p">.</span><span class="n">add_paragraph</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">feed</span><span class="p">(</span><span class="n">html</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">handle_starttag</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="n">attrs</span><span class="p">):</span>
        <span class="s">"""
        Override handle_starttag method
        """</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">run</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">paragraph</span><span class="p">.</span><span class="n">add_run</span><span class="p">()</span>

        <span class="k">if</span> <span class="n">tag</span> <span class="ow">in</span> <span class="p">[</span><span class="s">"ul"</span><span class="p">]:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">run</span><span class="p">.</span><span class="n">add_break</span><span class="p">()</span>
        <span class="k">if</span> <span class="n">tag</span> <span class="ow">in</span> <span class="p">[</span><span class="s">"li"</span><span class="p">]:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">run</span><span class="p">.</span><span class="n">add_text</span><span class="p">(</span><span class="sa">u</span><span class="s">'        </span><span class="se">\u2022</span><span class="s">    '</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">handle_endtag</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag</span><span class="p">):</span>
        <span class="s">"""
        Override handle_endtag method
        """</span>
        <span class="k">if</span> <span class="n">tag</span> <span class="ow">in</span> <span class="p">[</span><span class="s">"li"</span><span class="p">]:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">run</span><span class="p">.</span><span class="n">add_break</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">handle_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">data</span><span class="p">):</span>
        <span class="s">"""Override handle_data method"""</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">run</span><span class="p">.</span><span class="n">add_text</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</code></pre></div></div>

<p>The code above involves overriding a function in the HTMLParser class and using the paragraph’s run to customize its style based on the starting tag.</p>

<p>If a tag needs to break on the end, we add a break for it.</p>

<p>I then utilized this custom class in my view to handle the HTML content:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">build_document</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="s">"""Build content document"""</span>
    <span class="n">document</span> <span class="o">=</span> <span class="n">Document</span><span class="p">()</span>
    <span class="n">doc_html_parser</span> <span class="o">=</span> <span class="n">DocumentHTMLParser</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>

    <span class="c1"># add a header
</span>    <span class="n">document</span><span class="p">.</span><span class="n">add_heading</span><span class="p">(</span><span class="s">"This is a header"</span><span class="p">)</span>

    <span class="c1"># add a paragraph
</span>    <span class="n">document</span><span class="p">.</span><span class="n">add_paragraph</span><span class="p">(</span><span class="s">"This is a normal style paragraph"</span><span class="p">)</span>

    <span class="c1"># add a paragraph within an italic text then go on with a break.
</span>    <span class="n">paragraph</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="n">add_paragraph</span><span class="p">()</span>
    <span class="n">run</span> <span class="o">=</span> <span class="n">paragraph</span><span class="p">.</span><span class="n">add_run</span><span class="p">()</span>
    <span class="n">run</span><span class="p">.</span><span class="n">italic</span> <span class="o">=</span> <span class="bp">True</span>
    <span class="n">run</span><span class="p">.</span><span class="n">add_text</span><span class="p">(</span><span class="s">"text will have italic style"</span><span class="p">)</span>
    <span class="n">run</span><span class="p">.</span><span class="n">add_break</span><span class="p">()</span>

    <span class="c1"># with html content, call method add_paragraph_and_feed tui build content
</span>    <span class="n">html_content</span> <span class="o">=</span> <span class="s">"&lt;p&gt;Nice to see Prep note 2&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Prep note 2 content 1&lt;/li&gt;&lt;li&gt;Prep note 2 content 2&lt;/li&gt;&lt;/ul&gt;"</span>
    <span class="n">doc_html_parser</span><span class="p">.</span><span class="n">add_paragraph_and_feed</span><span class="p">(</span><span class="n">html_content</span><span class="p">)</span>
</code></pre></div></div>

<p>Here’s the resulting docx file from the HTML content:
<img src="/assets/images/2023/03/2023-03-export-docx-in-django-img-3-export-docx-html.webpp" alt="" /></p>

<h2 id="unit-test">Unit Test</h2>

<p>On the backend side, unit testing is a crucial component to protect your application. In my project, each pull request requires a minimum of 80% code coverage through testing, making unit testing a mandatory part of the development process. To aid in writing unit tests, we utilize libraries such as <a href="https://factoryboy.readthedocs.io/en/stable/">factory_boy</a> and <a href="https://docs.pytest.org/en/stable/">pytest</a>. If you’re unfamiliar with these libraries, you can check out the links provided before proceeding.</p>

<p>In this section, I won’t be covering how to use or write unit tests for a Django application. Instead, I will ensure that the exported docx file has the correct name and type, and that the file contains the expected content and style.</p>

<h3 id="test-content-response">Test content response</h3>

<p>I performed some basic checks on the exported file, such as verifying the response status, content type, and file name.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_export_docx_general</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="s">"""
    Ensure general content like
    status response, content type, file name exported correctly
    """</span>
    <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s">"export_docx"</span><span class="p">))</span>
    <span class="kn">import</span> <span class="nn">pdb</span><span class="p">;</span><span class="n">pdb</span><span class="p">.</span><span class="n">set_trace</span><span class="p">()</span>
</code></pre></div></div>

<p>By using <code class="language-plaintext highlighter-rouge">import pdb;pdb.set_trace()</code> after making the <code class="language-plaintext highlighter-rouge">GET</code> request in the unit test, I am able to inspect the current data.</p>

<p>Here is an example of what it looks like:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;django.http.response.StreamingHttpResponse object at 0x7fc392a61990&gt;
<span class="o">(</span>Pdb<span class="o">)</span> response.status_code
200
<span class="o">(</span>Pdb<span class="o">)</span> response.streaming_content
&lt;map object at 0x7fc3927aadd0&gt;
<span class="o">(</span>Pdb<span class="o">)</span> response.streaming_content.__dir__<span class="o">()</span>
<span class="o">[</span><span class="s1">'__getattribute__'</span>, <span class="s1">'__iter__'</span>, <span class="s1">'__next__'</span>, <span class="s1">'__new__'</span>, <span class="s1">'__reduce__'</span>, <span class="s1">'__doc__'</span>, <span class="s1">'__repr__'</span>, <span class="s1">'__hash__'</span>, <span class="s1">'__str__'</span>, <span class="s1">'__setattr__'</span>, <span class="s1">'__delattr__'</span>, <span class="s1">'__lt__'</span>, <span class="s1">'__le__'</span>, <span class="s1">'__eq__'</span>, <span class="s1">'__ne__'</span>, <span class="s1">'__gt__'</span>, <span class="s1">'__ge__'</span>, <span class="s1">'__init__'</span>, <span class="s1">'__reduce_ex__'</span>, <span class="s1">'__subclasshook__'</span>, <span class="s1">'__init_subclass__'</span>, <span class="s1">'__format__'</span>, <span class="s1">'__sizeof__'</span>, <span class="s1">'__dir__'</span>, <span class="s1">'__class__'</span><span class="o">]</span>
<span class="o">(</span>Pdb<span class="o">)</span> response.get<span class="o">(</span><span class="s2">"Content-Type"</span><span class="o">)</span>
<span class="s1">'application/vnd.openxmlformats-officedocument.wordprocessingm'</span>
<span class="o">(</span>Pdb<span class="o">)</span> response.get<span class="o">(</span><span class="s2">"Content-Disposition"</span><span class="o">)</span>
<span class="s1">'attachment;filename=Recipe_Pho_2021-04-15-14-34-09.docx'</span>
</code></pre></div></div>

<p>As seen in the previous code block, I can continue writing unit tests to check the exported file’s general content, such as the file’s content type, status code, and name.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_export_docx_general</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="s">"""
    Ensure general content like
    status response, content type, file name exported correctly
    """</span>
    <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s">"export_docx"</span><span class="p">))</span>
    <span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="n">status</span><span class="p">.</span><span class="n">HTTP_200_OK</span>
    <span class="k">assert</span> <span class="n">response</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"Content-Type"</span><span class="p">)</span> <span class="o">==</span> \
        <span class="s">"application/vnd.openxmlformats-officedocument.wordprocessingm"</span>
    <span class="n">filename</span> <span class="o">=</span> <span class="n">response</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"Content-Disposition"</span><span class="p">).</span><span class="n">split</span><span class="p">(</span><span class="s">"="</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
    <span class="k">assert</span> <span class="n">filename</span> <span class="o">==</span> <span class="s">"Test.docx"</span>
</code></pre></div></div>

<p>Please note the <code class="language-plaintext highlighter-rouge">response.streaming_content</code> object above which appears as a map object without any data for testing. Initially, I was unsure about how to test the content and style of the document accurately. Despite researching various options, I could not find a suitable solution. Eventually, I came up with a solution for testing the built document myself, which is as follows:</p>

<h3 id="test-document-content">Test document content</h3>

<p>I have created a function in the code called <code class="language-plaintext highlighter-rouge">build_document</code> to build the document, which is now ready for testing:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">build_document</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="s">"""Build content document"""</span>
    <span class="n">document</span> <span class="o">=</span> <span class="n">Document</span><span class="p">()</span>
    <span class="n">doc_html_parser</span> <span class="o">=</span> <span class="n">DocumentHTMLParser</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>

    <span class="c1"># with html content, call method add_paragraph_and_feed tui build content
</span>    <span class="n">html_content</span> <span class="o">=</span> <span class="s">"&lt;p&gt;Nice to see Prep note 2&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Prep note 2 content 1&lt;/li&gt;&lt;li&gt;Prep note 2 content 2&lt;/li&gt;&lt;/ul&gt;"</span>
    <span class="n">doc_html_parser</span><span class="p">.</span><span class="n">add_paragraph_and_feed</span><span class="p">(</span><span class="n">html_content</span><span class="p">)</span>
</code></pre></div></div>

<p>My solution was to directly call this function for testing on a mocked view.</p>

<p>Here’s how it appears in the test function:</p>

<pre><code class="language-pyhton">def test_build_document_for_docx(self):
    """Ensure document built content and style correctly"""
    # inline import just for you know where they are
    from django.http import HttpRequest
    from rest_framework.request import Request as DRFRequest

    # mock drf request
    request = HttpRequest()
    request.method = 'GET'
    drf_request = DRFRequest(request)
    drf_request.user = self.user

    # mock view with request
    view = ExportRecipesDocx()
    view.request = request

    # call function in view directly
    document = view.build_document()
    import pdb;pdb.set_trace()
</code></pre>

<p>Once again, I checked the document profile. As an example, I just have a personal curiosity and love to explore what they are 🥰.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>Pdb<span class="o">)</span> document
&lt;docx.document.Document object at 0x7fd5e65140a0&gt;
<span class="o">(</span>Pdb<span class="o">)</span> document._body.paragraphs
<span class="o">[</span>&lt;docx.text.paragraph.Paragraph object at 0x7fd5e5d5fb50&gt;]
<span class="o">(</span>Pdb<span class="o">)</span> document._body.paragraphs[0].runs
<span class="o">[</span>&lt;docx.text.run.Run object at 0x7fd5e5e419d0&gt;, &lt;docx.text.run.Run object at 0x7fd5e5eba6d0&gt;, &lt;docx.text.run.Run object at 0x7fd5e5eba410&gt;, &lt;docx.text.run.Run object at 0x7fd5e5eba3d0&gt;]
<span class="o">(</span>Pdb<span class="o">)</span> document._body.paragraphs[0].runs[0].text
<span class="s1">'Nice to see Prep note 2\n'</span>
<span class="o">(</span>Pdb<span class="o">)</span> document._body.paragraphs[0].runs[0].style.name
<span class="s1">'Default Paragraph Font'</span>
<span class="o">(</span>Pdb<span class="o">)</span> document._body.paragraphs[0].runs[0].style.priority
1
<span class="o">(</span>Pdb<span class="o">)</span> document._body.paragraphs[0].runsp[1].text
</code></pre></div></div>

<p>In my current <code class="language-plaintext highlighter-rouge">build_document</code> method, I create a paragraph and add some runs to it, while also inserting breaks where necessary based on the start and end tags of the HTML.</p>

<p>Below is the final version of my unit test for checking the document’s content and styles:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_build_document_for_docx</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="s">"""Ensure document built content and style correctly"""</span>
    <span class="c1"># mock request and view initialize like above code
</span>    <span class="c1"># ...
</span>    <span class="c1"># call function in view directly
</span>    <span class="n">document</span> <span class="o">=</span> <span class="n">view</span><span class="p">.</span><span class="n">build_document</span><span class="p">()</span>

    <span class="n">paragraphs</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="n">_body</span><span class="p">.</span><span class="n">paragraphs</span>
    <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">paragraphs</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>
    <span class="k">assert</span> <span class="n">paragraphs</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">style</span><span class="p">.</span><span class="n">name</span> <span class="o">==</span> <span class="s">"Normal"</span>
    <span class="k">assert</span> <span class="n">paragraphs</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">style</span><span class="p">.</span><span class="n">priority</span> <span class="ow">is</span> <span class="bp">None</span>
    <span class="k">assert</span> <span class="p">[</span>
        <span class="s">'Nice to see Prep note 2'</span><span class="p">,</span>
        <span class="s">'</span><span class="se">\n</span><span class="s">'</span><span class="p">,</span>
        <span class="s">'        •    Prep note 2 content 1</span><span class="se">\n</span><span class="s">'</span><span class="p">,</span>
        <span class="s">'        •    Prep note 2 content 2</span><span class="se">\n</span><span class="s">'</span>
    <span class="p">]</span> <span class="o">==</span> <span class="p">[</span><span class="n">item</span><span class="p">.</span><span class="n">text</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">paragraphs</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">runs</span><span class="p">]</span>
    <span class="k">assert</span> <span class="p">{</span><span class="bp">None</span><span class="p">}</span> <span class="o">==</span> <span class="p">{</span><span class="n">item</span><span class="p">.</span><span class="n">italic</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">paragraphs</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">runs</span><span class="p">}</span>
    <span class="k">assert</span> <span class="p">{</span><span class="bp">None</span><span class="p">}</span> <span class="o">==</span> <span class="p">{</span><span class="n">item</span><span class="p">.</span><span class="n">bold</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">paragraphs</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">runs</span><span class="p">}</span>
</code></pre></div></div>

<p>Exporting files in a Django app is a fascinating process, and Python has several libraries that are useful for handling content formats.</p>

<h2 id="final-word">Final word</h2>
<p>In this article, we discussed a straightforward example of exporting docx files within a Django app.</p>]]></content><author><name>Thanh Nguyen</name></author><category term="Django" /><category term="django" /><category term="export" /><category term="docx" /><category term="english" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Giới thiệu dịch vụ EC2 của AWS</title><link href="https://beautyoncode.com/aws/gioi-thieu-dich-vu-ec2-cua-aws/" rel="alternate" type="text/html" title="Giới thiệu dịch vụ EC2 của AWS" /><published>2023-03-18T00:00:00-04:00</published><updated>2023-03-18T00:00:00-04:00</updated><id>https://beautyoncode.com/aws/gioi-thieu-dich-vu-ec2-cua-aws</id><content type="html" xml:base="https://beautyoncode.com/aws/gioi-thieu-dich-vu-ec2-cua-aws/"><![CDATA[<p><img src="/images/2023/03/2023-03-gioi-thieu-dich-vu-ec2-cua-aws.webp" alt="" /></p>

<p><a href="https://aws.amazon.com/ec2/">EC2</a> là viết tắt của Elastic Compute Cloud, là một dịch vụ AWS thuộc loại Infrastructure as a Service (IaaS)</p>

<p>Hiện nay, EC2 cung cấp 4 nhóm dịch vụ chính bao gồm:</p>

<ol>
  <li>
    <p>Cho thuê (tạo) máy chủ ảo (EC2 instance)</p>
  </li>
  <li>
    <p>Lưu trữ dữ liệu trên máy chủ ảo (EBS – Elastic Block Store, EFS – Elastic File System)</p>
  </li>
  <li>
    <p>Phân phối tải trên nhiều máy chủ ảo (ELB – Elastic Load Balancing)</p>
  </li>
  <li>
    <p>Tự động scale số lượng máy chủ ảo (ASG – Auto Scaling Group)</p>
  </li>
</ol>

<h2 id="thuê-máy-chủ-ảo-ec2-instance">Thuê máy chủ ảo (EC2 instance)</h2>

<p>Khi thuê một máy chủ ảo, bạn có thể tạo máy với cấu hình mong muốn:</p>

<ul>
  <li>OS (operating system): hệ điều hành như Linux, MacOS, Window</li>
  <li>CPU (central processing unit): bộ xử lý trung tâm hay bộ vi xử lý (các loại chip như core i7, m1)</li>
  <li>RAM (random access memory): bộ nhớ tạm giúp CPU truy xuất lấy thông tin nhanh</li>
  <li>Các loại bộ nhớ kèm theo như:
    <ul>
      <li>bộ nhớ bên ngoài được gắn vào như EBS, EFS (các network drive)</li>
      <li>bộ nhớ bên trong như EC2 instance store</li>
      <li>card mạng</li>
      <li>tường lửa (security group)</li>
      <li>bootstrap script (EC2 user data) giúp boot máy tính khi lần đầu tạo</li>
    </ul>
  </li>
</ul>

<h2 id="lưu-trữ-dữ-liệu-trên-máy-chủ-ảo">Lưu trữ dữ liệu trên máy chủ ảo</h2>

<h3 id="ec2-instance-store">EC2 Instance Store</h3>

<p>EC2 Instance store là bộ nhớ bên trong máy EC2</p>

<h3 id="ebs---elastic-block-store">EBS - Elastic Block Store</h3>

<p>EBS volume là một network drive được gắn vào máy EC2 instance của bạn khi chạy. Mỗi EBS volume chỉ được gắn vào một EC2 tại một thời điểm. EBS volume nằm cố định ở AZ cụ thể.</p>

<p>Vì EBS volume chỉ nằm ở một AZ cụ thể nên nếu muốn di chuyển bộ nhớ này qua vùng AZ khác sẽ cần tạo EBS snapshot rồi sử dụng để tạo EBS volume ở AZ cần chuyển qua.</p>

<p>Có nhiều loại EBS volume khác nhau như gp2, gp3, io1, io2, stl, scl. Mỗi loại sẽ có các thông số về Size, Throughput, IOPS tương ứng.</p>

<p>EBS multi-attach loại io1, io2 có thể gắn vào nhiều máy EC2</p>

<h3 id="efs---elastic-file-system">EFS - Elastic File System</h3>

<p>EFS có thể gắn vào nhiều máy chủ EC2 ở nhiều AZ khác nhau.</p>

<p>EFS chỉ hỗ trợ máy Linux, thưởng sử dụng trong chia sẻ nội dung như web server, WordPress.</p>

<h2 id="phân-phối-tải-trên-nhiều-máy-chủ-ảo">Phân phối tải trên nhiều máy chủ ảo</h2>

<h3 id="khả-năng-mở-rộng">Khả năng mở rộng</h3>

<p><strong>Scalability</strong> (khả năng mở rộng) có nghĩa là hệ thống có thể tự xử lý tải.</p>

<p>Có 2 loại:</p>

<ul>
  <li>
    <p><strong>Vertical Scalability</strong>: chỉ việc tăng khả năng của máy chủ như CPU, RAM, bộ nhớ. Kiểu mở rộng này phổ biến với các hệ thống không phân tán, như cơ sở dữ liệu (RDS, ElastiCache).</p>
  </li>
  <li>
    <p><strong>Horizontal Scalability</strong>: chỉ việc tăng số lượng máy chủ.Kiểu mở rộng này phổ biến với các hệ thống phân tán, như ứng dụng web.</p>
  </li>
</ul>

<h3 id="lợi-ích-khi-sử-dụng-load-balancer">Lợi ích khi sử dụng load balancer</h3>

<ul>
  <li>giúp phân phối tải (traffic) đến nhiều máy ch</li>
  <li>ứng dụng chỉ có một địa chỉ DNS</li>
  <li>xử lý lỗi và thực hiện kiểm tra sức khỏa (health check) các máy chủ</li>
  <li>cung cấp dịch vụ SSL (HTTPS)</li>
  <li>có thể gắn người dùng với cookie</li>
  <li>tăng tính khả dụng (high availability) qua nhiều AZ</li>
  <li>tách các loại traffic khác nhau để xử lý</li>
</ul>

<h3 id="aws-load-balancers">AWS Load Balancers</h3>
<p>AWS cung cấp 3 loại Load Balancers là:</p>

<ul>
  <li>Classic Load Balancer (CLB)</li>
  <li>Application Load Balancer (ALB)</li>
  <li>Network Load Balancer (NLB)</li>
  <li>Gateway Load Balancer (GWLB)</li>
</ul>

<p>Người dùng khi truy cập đến ứng dụng sẽ đi qua security group của load balancer, sau đó đi đến máy chủ.</p>

<p>Bạn có thể truyền tải đến nhiều nhóm máy chủ được thiết lập theo target group.</p>
<h4 id="classic-load-balancer-clb">Classic Load Balancer (CLB)</h4>
<p><strong>Classic Load Balancer (CLB)</strong> (thế hệ cũ – không khuyến khích sử dụng) cho phép truyền tải với các giao thức HTTP, HTTPS, TCP, SSL (secure TCP).</p>

<h4 id="application-load-balancer-alb">Application Load Balancer (ALB)</h4>
<p><strong>Application Load Balancer (ALB)</strong> cho phép truyền tải với giao thức <strong>HTTP, HTTPS và WebSocket.</strong></p>

<p>Các truy cập có thể được phân loại dựa trên URL, hostname query string, headers, … để xác định đi đến target group nào. Các target group có thể là EC2 instances, ECS tasks, lambda function, private IP address</p>

<p>ALB phù hợp cho micro services và ứng dụng container-based.</p>

<p>ALB có hostname cố định, ví dụ xxx.region.elb.amazoneaws.com.</p>

<p>Health check hỗ trợ ở target group.</p>

<h4 id="network-load-balancer-nlb">Network Load Balancer (NLB)</h4>
<p><strong>Network Load Balancer (NLB)</strong> cho phép truyền tải với giao thức** TCP, TLS (secure TCP) và UDP.**</p>

<p>NLB có static IP cho mỗi AZ.</p>

<p>Các target group có thể là EC2 instances, địa chỉ IP, ALB</p>

<p>Health check hỗ trợ theo TCP, HTTP và HTTPS.</p>

<h4 id="gateway-load-balancer-gwlb">Gateway Load Balancer (GWLB)</h4>
<p><strong>Gateway Load Balancer (GWLB)</strong> cho phép truyền tải với giao thức IP.</p>

<p>GWLB cung cấp Transparent Network Gateway (vào, ra một lần) và Load Balancer (phân phối tải)</p>

<p>Các target group có thể là EC2 instances, địa chỉ IP</p>

<p><em>Ghi chú:</em> TCP (layer 4), HTTP &amp; HTTPS (layer 7), IP (layer 3)</p>

<p>Ngoài ra, để giữ cookie của người dùng và cho phép truy cập đến đúng máy chủ đã phục vụ trước đó, có thể dùng <strong>sticky session</strong>.</p>

<p>Còn nếu muốn phân phối tải với tỉ lệ đồng đều giữa cái AZ thì sử dụng <strong>cross-zone load balancing</strong>.</p>

<p>SSL certificate cho phép data giữa máy client và load balancer được mã hóa (encrypt)
Nếu bạn có nhiều SSL certificate cho từng loại máy chủ khác nhau thì <strong>SNI</strong> (Server Name Indication) giúp bạn xác định loại cert.</p>

<h2 id="tự-động-scale-số-lượng-máy-chủ">Tự động scale số lượng máy chủ</h2>

<p>Thực tế là không thể biết trước số tải (traffic) sẽ truy cập đến ứng dụng của bạn.</p>

<p>ASG – Auto Scaling Group giúp bạn:</p>

<ul>
  <li>tự động tăng số lượng máy chủ khi tải tăng</li>
  <li>tự động giảm số lượng máy chủ khi tải giảm</li>
  <li>đảm bảo số máy chủ tối đa và tối thiểu nhất định</li>
  <li>tự động tạo máy chủ mới với cấu hình định sẵn (launch template)</li>
  <li>tự động tạo lại nếu máy chủ bị tắt khi unhealthy</li>
</ul>

<p>Có thể kích hoạt ASG từ thông số của thông báo từ CloudWatch về chỉ số như Average CPU, hay tự tạo chỉ số.</p>

<p>Có thể tạo policies để kích hoạt ASG theo nhiều loại như:</p>

<ul>
  <li>Target tracking scaling: muốn CPU tầm 40%</li>
  <li>Simple / Step Scaling:
    <ul>
      <li>khi có cảnh báo từ CloudWatch với CPU &gt; 70% -&gt; thêm 2 máy chủ</li>
      <li>khi có cảnh báo từ CloudWatch với CPU &lt; 30% -&gt; bỏ bớt 1 máy chủ</li>
      <li>scheduled Actions: tăng hay giảm số máy chủ vào cuối tuần</li>
    </ul>
  </li>
</ul>

<hr />

<p>Bài viết giới thiệu đến bạn một số kiến thức cơ bản về dịch vụ EC2 cung cấp bởi AWS.</p>

<p>Bạn ghé trang chủ EC2 nếu muốn tìm hiểu thêm nhé.</p>]]></content><author><name>Thanh Nguyen</name></author><category term="AWS" /><category term="aws" /><category term="ec2" /><summary type="html"><![CDATA[EC2 là viết tắt của Elastic Compute Cloud, là một dịch vụ AWS thuộc loại Infrastructure as a Service (IaaS) Hiện nay, EC2 cung cấp 4 nhóm dịch vụ chính bao gồm: Cho thuê (tạo) máy chủ ảo (EC2 instance) Lưu trữ dữ liệu trên máy chủ ảo (EBS – Elastic Block Store, EFS – Elastic File System) Phân phối tải trên nhiều máy chủ ảo (ELB – Elastic Load Balancing) Tự động scale số lượng máy chủ ảo (ASG – Auto Scaling Group) Thuê máy chủ ảo (EC2 instance) Khi thuê một máy chủ ảo, bạn có thể tạo máy với cấu hình mong muốn: OS (operating system): hệ điều hành như Linux, MacOS, Window CPU (central processing unit): bộ xử lý trung tâm hay bộ vi xử lý (các loại chip như core i7, m1) RAM (random access memory): bộ nhớ tạm giúp CPU truy xuất lấy thông tin nhanh Các loại bộ nhớ kèm theo như: bộ nhớ bên ngoài được gắn vào như EBS, EFS (các network drive) bộ nhớ bên trong như EC2 instance store card mạng tường lửa (security group) bootstrap script (EC2 user data) giúp boot máy tính khi lần đầu tạo Lưu trữ dữ liệu trên máy chủ ảo EC2 Instance Store EC2 Instance store là bộ nhớ bên trong máy EC2 EBS - Elastic Block Store EBS volume là một network drive được gắn vào máy EC2 instance của bạn khi chạy. Mỗi EBS volume chỉ được gắn vào một EC2 tại một thời điểm. EBS volume nằm cố định ở AZ cụ thể. Vì EBS volume chỉ nằm ở một AZ cụ thể nên nếu muốn di chuyển bộ nhớ này qua vùng AZ khác sẽ cần tạo EBS snapshot rồi sử dụng để tạo EBS volume ở AZ cần chuyển qua. Có nhiều loại EBS volume khác nhau như gp2, gp3, io1, io2, stl, scl. Mỗi loại sẽ có các thông số về Size, Throughput, IOPS tương ứng. EBS multi-attach loại io1, io2 có thể gắn vào nhiều máy EC2 EFS - Elastic File System EFS có thể gắn vào nhiều máy chủ EC2 ở nhiều AZ khác nhau. EFS chỉ hỗ trợ máy Linux, thưởng sử dụng trong chia sẻ nội dung như web server, WordPress. Phân phối tải trên nhiều máy chủ ảo Khả năng mở rộng Scalability (khả năng mở rộng) có nghĩa là hệ thống có thể tự xử lý tải. Có 2 loại: Vertical Scalability: chỉ việc tăng khả năng của máy chủ như CPU, RAM, bộ nhớ. Kiểu mở rộng này phổ biến với các hệ thống không phân tán, như cơ sở dữ liệu (RDS, ElastiCache). Horizontal Scalability: chỉ việc tăng số lượng máy chủ.Kiểu mở rộng này phổ biến với các hệ thống phân tán, như ứng dụng web. Lợi ích khi sử dụng load balancer giúp phân phối tải (traffic) đến nhiều máy ch ứng dụng chỉ có một địa chỉ DNS xử lý lỗi và thực hiện kiểm tra sức khỏa (health check) các máy chủ cung cấp dịch vụ SSL (HTTPS) có thể gắn người dùng với cookie tăng tính khả dụng (high availability) qua nhiều AZ tách các loại traffic khác nhau để xử lý AWS Load Balancers AWS cung cấp 3 loại Load Balancers là: Classic Load Balancer (CLB) Application Load Balancer (ALB) Network Load Balancer (NLB) Gateway Load Balancer (GWLB) Người dùng khi truy cập đến ứng dụng sẽ đi qua security group của load balancer, sau đó đi đến máy chủ. Bạn có thể truyền tải đến nhiều nhóm máy chủ được thiết lập theo target group. Classic Load Balancer (CLB) Classic Load Balancer (CLB) (thế hệ cũ – không khuyến khích sử dụng) cho phép truyền tải với các giao thức HTTP, HTTPS, TCP, SSL (secure TCP). Application Load Balancer (ALB) Application Load Balancer (ALB) cho phép truyền tải với giao thức HTTP, HTTPS và WebSocket. Các truy cập có thể được phân loại dựa trên URL, hostname query string, headers, … để xác định đi đến target group nào. Các target group có thể là EC2 instances, ECS tasks, lambda function, private IP address ALB phù hợp cho micro services và ứng dụng container-based. ALB có hostname cố định, ví dụ xxx.region.elb.amazoneaws.com. Health check hỗ trợ ở target group. Network Load Balancer (NLB) Network Load Balancer (NLB) cho phép truyền tải với giao thức** TCP, TLS (secure TCP) và UDP.** NLB có static IP cho mỗi AZ. Các target group có thể là EC2 instances, địa chỉ IP, ALB Health check hỗ trợ theo TCP, HTTP và HTTPS. Gateway Load Balancer (GWLB) Gateway Load Balancer (GWLB) cho phép truyền tải với giao thức IP. GWLB cung cấp Transparent Network Gateway (vào, ra một lần) và Load Balancer (phân phối tải) Các target group có thể là EC2 instances, địa chỉ IP Ghi chú: TCP (layer 4), HTTP &amp; HTTPS (layer 7), IP (layer 3) Ngoài ra, để giữ cookie của người dùng và cho phép truy cập đến đúng máy chủ đã phục vụ trước đó, có thể dùng sticky session. Còn nếu muốn phân phối tải với tỉ lệ đồng đều giữa cái AZ thì sử dụng cross-zone load balancing. SSL certificate cho phép data giữa máy client và load balancer được mã hóa (encrypt) Nếu bạn có nhiều SSL certificate cho từng loại máy chủ khác nhau thì SNI (Server Name Indication) giúp bạn xác định loại cert. Tự động scale số lượng máy chủ Thực tế là không thể biết trước số tải (traffic) sẽ truy cập đến ứng dụng của bạn. ASG – Auto Scaling Group giúp bạn: tự động tăng số lượng máy chủ khi tải tăng tự động giảm số lượng máy chủ khi tải giảm đảm bảo số máy chủ tối đa và tối thiểu nhất định tự động tạo máy chủ mới với cấu hình định sẵn (launch template) tự động tạo lại nếu máy chủ bị tắt khi unhealthy Có thể kích hoạt ASG từ thông số của thông báo từ CloudWatch về chỉ số như Average CPU, hay tự tạo chỉ số. Có thể tạo policies để kích hoạt ASG theo nhiều loại như: Target tracking scaling: muốn CPU tầm 40% Simple / Step Scaling: khi có cảnh báo từ CloudWatch với CPU &gt; 70% -&gt; thêm 2 máy chủ khi có cảnh báo từ CloudWatch với CPU &lt; 30% -&gt; bỏ bớt 1 máy chủ scheduled Actions: tăng hay giảm số máy chủ vào cuối tuần Bài viết giới thiệu đến bạn một số kiến thức cơ bản về dịch vụ EC2 cung cấp bởi AWS. Bạn ghé trang chủ EC2 nếu muốn tìm hiểu thêm nhé.]]></summary></entry></feed>