Part 3: Development, Testing, and Code Quality

Introduction

This is where planning meets reality. In this part, I'll walk through my actual development workflow—from writing the first line of code to ensuring quality through testing and code review.

Let's build that webhook handler from Part 2.

Actually Writing Code

The Development Flow

spinner

Step 1: Write the Test First

Here's my actual first test for the webhook handler:

// tests/unit/webhookHandler.test.ts
import { describe, it, expect, vi } from 'vitest';
import { WebhookHandler } from '../../src/services/webhookHandler';
import crypto from 'crypto';

describe('WebhookHandler', () => {
  const handler = new WebhookHandler({
    secret: 'whsec_test_secret',
  });

  describe('verifySignature', () => {
    it('should accept valid Stripe signature', () => {
      const payload = JSON.stringify({ type: 'payment_intent.succeeded' });
      const timestamp = Math.floor(Date.now() / 1000);
      
      // Create valid signature
      const signedPayload = `${timestamp}.${payload}`;
      const signature = crypto
        .createHmac('sha256', 'whsec_test_secret')
        .update(signedPayload)
        .digest('hex');
      
      const headers = {
        'stripe-signature': `t=${timestamp},v1=${signature}`,
      };

      expect(() => handler.verifySignature(payload, headers)).not.toThrow();
    });

    it('should reject invalid signature', () => {
      const payload = JSON.stringify({ type: 'payment_intent.succeeded' });
      const headers = {
        'stripe-signature': 't=123456789,v1=invalidsignature',
      };

      expect(() => handler.verifySignature(payload, headers))
        .toThrow('Invalid signature');
    });

    it('should reject expired timestamps (>5 min old)', () => {
      const payload = JSON.stringify({ type: 'payment_intent.succeeded' });
      const oldTimestamp = Math.floor(Date.now() / 1000) - 400; // 6 mins ago
      
      const signedPayload = `${oldTimestamp}.${payload}`;
      const signature = crypto
        .createHmac('sha256', 'whsec_test_secret')
        .update(signedPayload)
        .digest('hex');
      
      const headers = {
        'stripe-signature': `t=${oldTimestamp},v1=${signature}`,
      };

      expect(() => handler.verifySignature(payload, headers))
        .toThrow('Timestamp too old');
    });
  });
});

Why test first?

  1. Forces me to think about the API

  2. Documents expected behavior

  3. Prevents over-engineering

  4. Gives me confidence to refactor

Step 2: Implement Minimum Code

Now I write just enough to make tests pass:

Tests pass! But code isn't great yet. That's okay—refactor next.

Step 3: Refactor

Now I improve code quality without changing behavior:

What improved?

  • ✅ Extracted methods (easier to test and understand)

  • ✅ Added logging and metrics

  • ✅ Timing-safe comparison (security)

  • ✅ Idempotency check

  • ✅ Better error handling

  • ✅ Type safety with interfaces

Tests still pass! This is the power of TDD.

The Fastify Route Handler

Now connect it to HTTP:

Testing & Debugging

Testing Strategy

spinner

Unit Tests (Continued)

Testing error scenarios:

Integration Tests

Testing with real Redis:

Debugging Techniques

1. Strategic Console Logging

2. VS Code Debugger

Set breakpoint in VS Code, start debug mode (F5), make request:

3. Request Replay Debugging

Save failing requests for replay:

Later, replay exact request:

4. Error Tracking with Sentry

When production error occurs, Sentry shows:

  • Full stack trace

  • Request context

  • Breadcrumbs (recent logs)

  • User session replay

Browser Automation

For E2E testing, I use Playwright:

Setup

Visual Regression Testing

Run tests:

Code Review

My Code Review Checklist

spinner

What I Look For

1. Correctness

2. Test Coverage

3. Security

4. Performance

My Code Review Comments (Examples)

Style: Nitpick (optional changes)

Must Fix: Correctness issue

Question: Understanding needed

Key Takeaways

  1. Write tests first: TDD prevents over-engineering and gives confidence

  2. Refactor ruthlessly: Make code readable for your future self

  3. Debug systematically: Logging, breakpoints, request replay

  4. Automate E2E tests: Catches integration issues early

  5. Review with intent: Look for correctness, security, performance

What's Next

In Part 4, we'll build APIs and integrate MCP:

  • RESTful API design

  • GraphQL considerations

  • Model Context Protocol (MCP) integration

  • API documentation and versioning


Code quality isn't just about working—it's about working well. See you in Part 4.

Last updated