<?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://peterhrynkow.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://peterhrynkow.com/" rel="alternate" type="text/html" /><updated>2026-02-13T08:47:48+00:00</updated><id>https://peterhrynkow.com/feed.xml</id><title type="html">Peter Hrynkow - Vancouver Tech Leader</title><subtitle>Peter Hrynkow is an engineering leader focused on AI enablement, automation, and developer productivity.
</subtitle><author><name>Peter Hrynkow</name></author><entry><title type="html">I Built a Custom Event Router, Then Deleted It: Lessons in Knowing When to Stop Building</title><link href="https://peterhrynkow.com/architecture/2025/02/15/i-built-an-event-router-then-deleted-it.html" rel="alternate" type="text/html" title="I Built a Custom Event Router, Then Deleted It: Lessons in Knowing When to Stop Building" /><published>2025-02-15T00:00:00+00:00</published><updated>2025-02-15T00:00:00+00:00</updated><id>https://peterhrynkow.com/architecture/2025/02/15/i-built-an-event-router-then-deleted-it</id><content type="html" xml:base="https://peterhrynkow.com/architecture/2025/02/15/i-built-an-event-router-then-deleted-it.html"><![CDATA[<p>Every booking on my platform kicks off a chain of work – confirmation emails, calendar events, reminders – all handled by n8n, a visual workflow tool. It worked fine. But in late January, I decided I could do better.</p>

<p>I committed a custom event processing system: an event emitter, a task scheduler, an email sender – over a dozen modules, documented across five markdown files totaling 4,000+ lines.</p>

<p>Six days later, I committed <code class="language-plaintext highlighter-rouge">back to n8n!!!</code> with three exclamation marks. Two days after that, I deleted all of it. Over 3,000 lines of application code, gone.</p>

<p>This is what I learned about the difference between good architecture and appropriate architecture.</p>

<h2 id="what-i-was-trying-to-solve">What I Was Trying to Solve</h2>

<p>The platform processes inbound work: form submissions, bookings, confirmation emails, calendar events, reminders. Before the event system, all of this orchestration lived in n8n. Workflows were easy to build, execution history was visible, and integrations with Gmail and Google Calendar were pre-built.</p>

<p>But n8n was a separate system with its own database, its own deployment, its own failure modes. Every workflow was a black box from the platform’s perspective. I couldn’t query “what happened after this booking was created?” from my application database. The event-driven purist in me wanted a first-class event system: emit events, route them through handlers, track attempts, support replay.</p>

<p>So I built one.</p>

<h2 id="what-i-built">What I Built</h2>

<p>The system had five major components:</p>

<p><strong>Event emission.</strong> Typed events (<code class="language-plaintext highlighter-rouge">allocation_created</code>, <code class="language-plaintext highlighter-rouge">allocation_modified</code>, <code class="language-plaintext highlighter-rouge">form_submitted</code>) stored in a database table with idempotency via deduplication keys.</p>

<p><strong>Event processing.</strong> An edge function that resolved which handlers should run based on event type and business configuration, executing them in sequence. Each execution was recorded as an <code class="language-plaintext highlighter-rouge">event_attempt</code> with status tracking.</p>

<p><strong>Task scheduling.</strong> Events triggered scheduled tasks – pickup reminders, return reminders, status transitions – with edge case handling for past dates.</p>

<p><strong>Email sending.</strong> A template resolution engine that pulled templates from capability settings, resolved variables against event data, and sent through Gmail via OAuth.</p>

<p><strong>Calendar integration.</strong> Created and updated Google Calendar events when allocations were created or modified.</p>

<p>Events were immutable and replayable. Handlers were isolated and testable. Attempt tracking gave complete observability. Deduplication prevented double-processing. It was, by any technical measure, well-designed.</p>

<p>And it came together fast. I leaned heavily on AI coding assistants for this build – the emitter, the processor, the scheduler, all produced and iterated quickly. What might have taken two weeks of solo work compressed into days. I was proud of the system and the velocity.</p>

<h2 id="why-i-deleted-it">Why I Deleted It</h2>

<p>Within a week of running the system end-to-end, three things became clear:</p>

<p><strong>1. Debugging was miserable.</strong> When a confirmation email didn’t send, I had to trace through: Was the event emitted? Check the event table. Was it processed? Check the event_attempt table. Did the handler succeed? Read the error JSON. Was the template correct? Check the capability settings. Was the OAuth token valid? Check the integration table.</p>

<p>With n8n, I opened the execution history, saw the green and red nodes, clicked the failed one, and read the error. Done.</p>

<p>The irony was brutal. I’d built the event system partly for <em>observability</em>. But observability isn’t debuggability. A complete audit trail in database rows doesn’t help when you need to understand <em>why</em> something failed. Visual execution history – data flowing through nodes, with errors highlighted inline – is genuinely superior for diagnosing integration failures.</p>

<p><strong>2. Edge cases multiplied.</strong> The custom system handled the happy path well. But real-world orchestration is mostly edge cases. What happens when a calendar event needs updating but the OAuth token expired? What about partial failures where the email sent but the calendar update didn’t? What about timezone conversions for scheduled tasks?</p>

<p>Each edge case required code. In n8n, each required dragging a node and configuring it. The velocity difference was enormous.</p>

<p><strong>3. I was rebuilding n8n, badly.</strong> The moment I caught myself writing retry logic with exponential backoff for failed email sends, I stopped. I was building a workflow engine. n8n <em>is</em> a workflow engine, maintained by a team dedicated to exactly this problem. My version would always be worse: fewer integrations, worse error handling, no visual builder, no execution history UI.</p>

<p>Every hour spent on orchestration infrastructure was an hour not spent on the product. For an early-stage platform with one developer, that’s a fatal misallocation.</p>

<h2 id="the-commit-that-changed-everything">The Commit That Changed Everything</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>102b67b  back to n8n!!!
</code></pre></div></div>

<p>I brought n8n back the same day. Docker Compose setup, database schema restored, handlers migrated back to visual workflows. Two days later, <code class="language-plaintext highlighter-rouge">move more logic back to n8n</code>. Then a week of cleanup commits: every event processing file, every handler, every scheduler, every documentation file. The emitter, the processor, the scheduler, the email sender – all of it. Over 3,000 lines of TypeScript and 4,000 lines of documentation, gone.</p>

<p>It felt great.</p>

<h2 id="what-i-kept">What I Kept</h2>

<p>Not everything was thrown away. The core operations layer stayed: synchronous, request-in/response-out handlers for creating requests, managing allocations, and handling tasks. Pure functions that accept parameters, validate against a schema, call a database RPC, and return a result. No events, no async processing, no orchestration.</p>

<p>The database tables for events still exist in the migration history. I didn’t write a migration to drop them. They sit there empty – a reminder.</p>

<p>The lesson crystallized into a principle: <strong>operations should be synchronous and deterministic. Orchestration should be delegated to a purpose-built tool.</strong></p>

<h2 id="the-hybrid-architecture">The Hybrid Architecture</h2>

<p>What I ended up with is a hybrid that plays to each tool’s strength:</p>

<p><strong>Edge functions handle direct operations.</strong> Form submissions, allocation creation, resource CRUD. Synchronous, validated, returning immediately. TypeScript on Deno – fast, typed, easy to test.</p>

<p><strong>n8n handles asynchronous orchestration.</strong> After a booking is created, n8n orchestrates: confirmation email, calendar event, reminders. It handles retries, OAuth refresh, and integration-specific errors with pre-built nodes.</p>

<p><strong>The platform doesn’t know about orchestration internals.</strong> It fires an operation and gets a result. What happens afterward is n8n’s concern. The boundary is clean.</p>

<h2 id="lessons-for-staff-engineers">Lessons for Staff+ Engineers</h2>

<p><strong>1. Observability and debuggability are not the same thing.</strong> A complete event log is great for auditing. But when something breaks at 2 AM, you want a visual execution trace with actual data at each step, not <code class="language-plaintext highlighter-rouge">SELECT * FROM event_attempt WHERE status = 'failed'</code>. Know which one you actually need.</p>

<p><strong>2. “I could build this” is not a reason to build it.</strong> The question isn’t capability – it’s opportunity cost. Every hour spent on orchestration infrastructure is an hour not spent on the product’s actual differentiator.</p>

<p><strong>3. Deleting code is an engineering skill.</strong> Removing 3,000+ lines of working code was one of the hardest and best things I did. The codebase got simpler, the deployment got simpler, and debugging got dramatically better. Sunk cost is real, and overcoming it is a skill.</p>

<p><strong>4. Match your abstraction level to your stage.</strong> A custom event-driven architecture makes sense at scale, with a dedicated infrastructure team. For an early-stage product with one developer, it’s over-engineering of the highest order. I knew this. I built it anyway because it was intellectually satisfying. The three exclamation marks in that commit message were aimed at myself.</p>

<p><strong>5. AI coding tools are most dangerous in senior hands, not junior ones.</strong> The conventional wisdom is that AI assistants are risky for junior engineers who can’t evaluate the output. This experience convinced me it’s the opposite. Remember the velocity I mentioned? The AI produced genuinely good code – well-typed, well-structured, idiomatic. That was the problem. A junior engineer generating messy code with AI would have hit friction quickly: things wouldn’t compose, they’d get stuck, they’d ask for help. I generated a clean, well-architected system that <em>worked perfectly</em> and shouldn’t have existed. AI didn’t undermine my engineering judgment – it outran it. Ironically, the low cost of producing this code made deleting it easy – had I hand-written it over two weeks, the sunk cost instinct would have been far worse. But that’s exactly the trap: AI removes the friction that normally forces you to question whether a project is worth continuing. When building is nearly free, you don’t stop to ask “should I?” until you’re already done. The industry worries about AI generating bad code. The more insidious risk is AI generating <em>good</em> code so fast that experienced engineers never hit the natural pause where judgment kicks in.</p>

<h2 id="the-meta-lesson">The Meta-Lesson</h2>

<p>The best architecture isn’t the most technically elegant one. It’s the one that lets you ship the right thing at the right time. I spent a week building infrastructure I didn’t need, then another week deleting it. The product didn’t advance during either week.</p>

<p>The platform is better now – not because the event system was bad, but because the platform is <em>simpler</em>, and simplicity compounds. Fewer moving parts, fewer failure modes, faster debugging. That’s worth more than architectural purity.</p>

<p>Sometimes the best engineering decision is <code class="language-plaintext highlighter-rouge">git rm -r</code>.</p>

<hr />

<p><em>This is Part 3 of a three-part series. <a href="/ai/architecture/2025/02/01/schema-driven-platforms.html">Part 1</a> covered schema-driven architecture. <a href="/ai/architecture/2025/02/08/ai-generates-configuration-not-code.html">Part 2</a> covered using AI for configuration generation. This post covered knowing when to delete your own work.</em></p>]]></content><author><name>Peter Hrynkow</name></author><category term="architecture" /><summary type="html"><![CDATA[Every booking on my platform kicks off a chain of work – confirmation emails, calendar events, reminders – all handled by n8n, a visual workflow tool. It worked fine. But in late January, I decided I could do better.]]></summary></entry><entry><title type="html">AI Generates Configuration, Not Code: How I Use LLMs to Build Product Capabilities</title><link href="https://peterhrynkow.com/ai/architecture/2025/02/08/ai-generates-configuration-not-code.html" rel="alternate" type="text/html" title="AI Generates Configuration, Not Code: How I Use LLMs to Build Product Capabilities" /><published>2025-02-08T00:00:00+00:00</published><updated>2025-02-08T00:00:00+00:00</updated><id>https://peterhrynkow.com/ai/architecture/2025/02/08/ai-generates-configuration-not-code</id><content type="html" xml:base="https://peterhrynkow.com/ai/architecture/2025/02/08/ai-generates-configuration-not-code.html"><![CDATA[<p>There’s a wave of “app generators” promising that anyone – not just developers – can describe what they want and get a working application. The pitch is compelling, but the reality is that these tools generate application code with few guardrails. The output is deployed and you hope the tests catch the problems – assuming tests even exist.</p>

<p>My concern isn’t with developers using AI to write code. That’s a productivity tool with a human in the loop who understands what’s being produced. The problem is when code generation is the <em>product</em>, aimed at end users who can’t review what the AI wrote and have no way to verify it’s correct.</p>

<p>I took a different approach. In the platform I’ve been building, <strong>AI generates validated configuration – not executable code.</strong> The configuration is mechanically validated against strict schemas before it ever touches production. If the AI produces something invalid, it’s a schema error, not a production incident.</p>

<p>This distinction – configuration vs. code – is the difference between AI as a reliable product feature and AI as a liability.</p>

<h2 id="the-setup-why-configuration-generation-works">The Setup: Why Configuration Generation Works</h2>

<p>In <a href="/ai/architecture/2025/02/01/schema-driven-platforms.html">Part 1</a>, I described building a schema-driven platform where every domain model, form, and settings page is defined by JSON schemas. Adding a new capability to the platform means authoring a bundle of JSON files that conform to a meta-schema.</p>

<p>The interesting property of this architecture is that <strong>the output space is fully constrained.</strong> An LLM generating a capability bundle can only produce:</p>
<ul>
  <li>JSON Schema objects with specific property types</li>
  <li>UI layout definitions using a fixed vocabulary of layout elements</li>
  <li>Form definitions with a known set of field types and options</li>
  <li>Settings with declared defaults and enumerations</li>
</ul>

<p>There’s no arbitrary code. No function definitions. No control flow. Just structured data that either passes validation or doesn’t.</p>

<h2 id="the-generation-pipeline">The Generation Pipeline</h2>

<p>Here’s what actually happens when a new business onboards:</p>

<p><strong>Step 1: Natural Language Requirements.</strong> The user describes their business in plain English. “I’m a drywall contractor. I need to track job sites, manage material orders, and let customers request quotes online.”</p>

<p><strong>Step 2: Structured Extraction.</strong> The LLM (Claude) extracts structured requirements: industry vertical, core entities, relationships, booking/allocation needs, form intents. This is a constrained extraction task – the model maps free text to a known taxonomy of platform capabilities.</p>

<p><strong>Step 3: Bundle Generation.</strong> Using the extracted requirements and the platform’s meta-schema as context, the LLM generates a complete capability bundle. This includes:</p>
<ul>
  <li>Resource type schemas (the data models)</li>
  <li>UI layouts for create/edit forms</li>
  <li>List configurations for table views</li>
  <li>Public form schemas with field validation</li>
  <li>Parameter bindings that map form fields to backend operations</li>
  <li>Settings schemas with sensible defaults</li>
</ul>

<p><strong>Step 4: Mechanical Validation.</strong> The generated bundle goes through the exact same validation pipeline as human-authored bundles. AJV validates against the meta-schema. Every sub-schema is checked. Invalid bundles are rejected with specific error messages.</p>

<p><strong>Step 5: Storage and Activation.</strong> Valid bundles are stored as versioned, immutable records. Each business owns its bundle. The bundle is activated, and the generic UI immediately renders the new capability.</p>

<p>The entire pipeline runs in about 30 seconds. From “I run a yoga studio” to a live system with class scheduling, student management, and booking forms.</p>

<h2 id="what-the-meta-schema-actually-constrains">What the Meta-Schema Actually Constrains</h2>

<p>The power of this approach lives in the meta-schema. It’s a 1,600+ line JSON Schema document that defines:</p>

<p><strong>Resource types must declare traits.</strong> A resource is <code class="language-plaintext highlighter-rouge">allocatable</code> (can be booked), <code class="language-plaintext highlighter-rouge">temporal</code> (has time-based availability), <code class="language-plaintext highlighter-rouge">quantified</code> (has inventory), etc. These traits determine which platform behaviors apply. The AI can’t invent new platform behaviors – it can only compose existing ones.</p>

<p><strong>Forms must declare intent.</strong> Every form has an <code class="language-plaintext highlighter-rouge">intent</code>: <code class="language-plaintext highlighter-rouge">create_allocation</code>, <code class="language-plaintext highlighter-rouge">cancel_allocation</code>, <code class="language-plaintext highlighter-rouge">modify_allocation</code>, or <code class="language-plaintext highlighter-rouge">create_inquiry</code>. The intent determines which backend operation runs on submission. The AI doesn’t generate backend logic – it maps form fields to existing operations through a parameter binding DSL.</p>

<p><strong>Parameter bindings use a fixed expression language.</strong> Form fields map to RPC parameters through a small set of expressions: field references, literal values, runtime context lookups, time arithmetic, conditional switches. This is expressive enough for real-world forms but constrained enough that every expression is mechanically evaluable.</p>

<p><strong>Settings schemas must use known types and enumerations.</strong> The AI can’t introduce arbitrary behavior flags. Settings must conform to what the platform can actually configure.</p>

<h2 id="why-this-is-better-than-code-generation">Why This Is Better Than Code Generation</h2>

<p>I’ve seen teams try to use LLMs to generate React components, API endpoints, or database queries. The fundamental problem is verification. How do you know the generated code is correct? You need tests, code review, and runtime monitoring – the same pipeline you use for human-written code, except now you’re doing it for output you didn’t write and may not fully understand.</p>

<p>Configuration generation sidesteps this entirely:</p>

<p><strong>Validation is mechanical.</strong> Either the bundle conforms to the meta-schema or it doesn’t. There are no subtle runtime bugs.</p>

<p><strong>The blast radius is bounded.</strong> A bad configuration means one business sees incorrect field labels or a missing form section. It doesn’t mean a SQL injection or a broken API for all customers.</p>

<p><strong>Iteration is cheap.</strong> If the generated bundle isn’t quite right, regenerate with adjusted requirements. Each generation produces a new immutable version. There’s no merge conflict, no code review, no deployment.</p>

<p><strong>The AI gets better at the right task.</strong> When you use an LLM for structured generation against a schema, you can measure accuracy in hard terms: what percentage of generations pass validation on the first attempt? What percentage require human correction? These metrics are actionable. “Did the generated code work?” is much harder to quantify.</p>

<h2 id="the-feedback-loop-validation-errors-as-training-signal">The Feedback Loop: Validation Errors as Training Signal</h2>

<p>When a generated bundle fails validation, the error messages are specific and actionable: “Resource type ‘yoga_class’ declares trait ‘allocatable’ but schema includes property ‘qty_available’ – platform-managed fields must not appear in resource schemas.”</p>

<p>These errors serve two purposes:</p>
<ol>
  <li><strong>LLM self-correction.</strong> The errors are fed back to the LLM for a retry. In practice, first-attempt validation pass rates are high, and the retry loop catches most remaining issues.</li>
  <li><strong>Meta-schema refinement.</strong> Repeated generation failures in the same area signal that the meta-schema is ambiguous or that the LLM needs better prompting. This feedback loop has driven several meta-schema revisions that improved both human and AI authoring.</li>
</ol>

<h2 id="the-staff-engineering-insight">The Staff Engineering Insight</h2>

<p>The decision to use AI for configuration generation wasn’t primarily an AI decision. It was an <em>architecture</em> decision. The schema-driven platform had to exist first. The meta-schemas had to be rigorous. The validation pipeline had to be airtight. The generic UI had to be complete.</p>

<p>AI was the <em>last</em> piece, not the first. And it only worked because every preceding layer was designed with constraints that make the AI’s job tractable.</p>

<p>This is the pattern I’d advocate for any team looking to integrate generative AI into their product:</p>

<ol>
  <li><strong>Define the output space formally.</strong> Use schemas, grammars, type systems – whatever makes the valid output mechanically verifiable.</li>
  <li><strong>Build the validation pipeline first.</strong> If you can’t mechanically verify the output, you don’t have an AI feature. You have an AI experiment.</li>
  <li><strong>Make the AI generate data, not behavior.</strong> Configuration, schemas, mappings, templates – things that are declarative and inspectable. Not code that needs to be understood to be trusted.</li>
  <li><strong>Version and isolate.</strong> Every AI generation should produce an immutable, versioned artifact scoped to a single tenant. No shared mutable state.</li>
</ol>

<p>The goal isn’t to use AI to write your platform. It’s to design your platform so that AI can configure it safely.</p>

<h2 id="the-uncomfortable-industry-implication">The Uncomfortable Industry Implication</h2>

<p>There’s a consequence of this architecture that I think most of the industry is ignoring: <strong>AI code generation products are racing toward commodity, while AI configuration generation builds compounding advantage.</strong></p>

<p>Consider the trajectories. Code generation gets less differentiated over time – as models improve, everyone’s AI-generated code converges in quality. The code itself becomes interchangeable. If your product is “AI writes your app,” every foundation model improvement erodes your position, because your competitors get the same improvement for free.</p>

<p>Configuration generation runs in the opposite direction. Every generation tests and refines the meta-schema. Every schema revision makes future generations more accurate. The constraint system <em>is</em> the moat – and it’s a moat that deepens with use rather than eroding with time.</p>

<p>The industry is fixated on making AI generate code faster. But speed of generation was never the bottleneck. The bottleneck is <em>verification</em> – knowing that what the AI produced is correct. Code generation leaves verification as a human problem: review, test, monitor, hope. Configuration generation reduces verification to a mechanical check that runs in milliseconds. That’s not an incremental improvement. It’s a category difference.</p>

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

<p>The current system generates complete capability bundles from scratch. The next frontier is <em>incremental modification</em>: a business owner saying “I want to add a ‘dietary restrictions’ field to my booking form” and having the AI produce a v2 bundle with that change, validated against the same meta-schemas, without touching anything else.</p>

<p>This is harder than initial generation because it requires understanding the existing bundle, making a targeted change, and preserving everything else. But the architecture supports it – bundles are versioned, immutable, and mechanically diffable.</p>

<hr />

<p><em>This is Part 2 of a three-part series. <a href="/ai/architecture/2025/02/01/schema-driven-platforms.html">Part 1</a> covered schema-driven platform architecture. <a href="/architecture/2025/02/15/i-built-an-event-router-then-deleted-it.html">Part 3</a> covers what I learned from building a custom event-driven architecture, then deleting it.</em></p>]]></content><author><name>Peter Hrynkow</name></author><category term="ai" /><category term="architecture" /><summary type="html"><![CDATA[There’s a wave of “app generators” promising that anyone – not just developers – can describe what they want and get a working application. The pitch is compelling, but the reality is that these tools generate application code with few guardrails. The output is deployed and you hope the tests catch the problems – assuming tests even exist.]]></summary></entry><entry><title type="html">Schema-Driven Platforms: Why JSON Schema Is the Most Underrated Tool in Your Stack</title><link href="https://peterhrynkow.com/ai/architecture/2025/02/01/schema-driven-platforms.html" rel="alternate" type="text/html" title="Schema-Driven Platforms: Why JSON Schema Is the Most Underrated Tool in Your Stack" /><published>2025-02-01T00:00:00+00:00</published><updated>2025-02-01T00:00:00+00:00</updated><id>https://peterhrynkow.com/ai/architecture/2025/02/01/schema-driven-platforms</id><content type="html" xml:base="https://peterhrynkow.com/ai/architecture/2025/02/01/schema-driven-platforms.html"><![CDATA[<p>I’ve spent the last several months building a multi-tenant SaaS platform from scratch. The kind where every customer needs slightly different data models, different forms, different workflows. The kind that, done wrong, turns into an unmaintainable mess of <code class="language-plaintext highlighter-rouge">if (tenant.type === 'yoga_studio')</code> branches.</p>

<p>Early on, I made a decision that shaped everything that followed: <strong>JSON Schema would be the single source of truth for validation, UI generation, and runtime behavior.</strong> Not just for API validation. For <em>everything</em>.</p>

<p>That decision turned out to be the best architectural call I’ve made in years.</p>

<h2 id="the-problem-vertical-fields-are-a-trap">The Problem: Vertical Fields Are a Trap</h2>

<p>Every multi-tenant platform eventually faces the same temptation. A customer needs a “color” field. Another needs “square footage.” Another needs “session duration.” The easy path is to add columns to your database tables, write custom UI for each, and move on.</p>

<p>This works for two customers. By the fifth, you’re drowning. Every new customer type requires migrations, new form components, new list columns, new validation logic. Your “platform” is really just a collection of bespoke applications sharing a login page.</p>

<p>I’d seen this pattern destroy codebases at previous companies. So I drew a hard line: <strong>no vertical fields in core tables.</strong> Domain-specific data lives in JSONB columns, typed entirely by schemas.</p>

<h2 id="the-architecture-one-schema-three-jobs">The Architecture: One Schema, Three Jobs</h2>

<p>Here’s the core insight that makes this work. A single JSON Schema definition does triple duty:</p>

<p><strong>1. Validation.</strong> AJV validates data at every boundary – form submissions, API calls, database writes. The schema <em>is</em> the contract.</p>

<p><strong>2. UI Generation.</strong> The same schema drives form rendering. A <code class="language-plaintext highlighter-rouge">string</code> with an <code class="language-plaintext highlighter-rouge">enum</code> becomes a dropdown. A <code class="language-plaintext highlighter-rouge">number</code> with <code class="language-plaintext highlighter-rouge">minimum</code> and <code class="language-plaintext highlighter-rouge">maximum</code> gets range validation. A <code class="language-plaintext highlighter-rouge">boolean</code> renders as a toggle. Combined with a UI layout schema (using JSONForms conventions), you get full CRUD interfaces without writing a single component.</p>

<p><strong>3. List Configuration.</strong> A companion <code class="language-plaintext highlighter-rouge">list</code> schema defines table columns, search paths, sort defaults, and cell formatting. The same generic list component renders any entity type.</p>

<p>The result: adding a new entity type to the platform means authoring four JSON files (schema, UI layout, list config, metadata). No migrations. No new components. No deployments.</p>

<h2 id="what-i-actually-built">What I Actually Built</h2>

<p>The platform has a generic resource system. Every domain object – whether it’s an equipment item, a class session, a service offering – is stored in the same table with a JSONB <code class="language-plaintext highlighter-rouge">data</code> column. The resource’s <code class="language-plaintext highlighter-rouge">type_key</code> points to its schema definition.</p>

<p>The frontend has exactly three resource views: list, form, and detail. They’re completely generic. They accept schemas as props and render accordingly.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ResourceListView  + list.json   = any table
ResourceFormView  + schema.json + ui.json = any form
ResourceDetailView + ui.json    = any detail page
</code></pre></div></div>

<p>Settings work the same way. Each capability declares a settings schema, and the settings UI is auto-generated. No settings page is hand-coded.</p>

<p>Public-facing forms work the same way. A form schema defines fields, validation, and dynamic options. The same rendering pipeline produces customer-facing booking forms, inquiry forms, cancellation flows – all from JSON.</p>

<h2 id="the-hard-parts-nobody-talks-about">The Hard Parts Nobody Talks About</h2>

<p>This isn’t a free lunch. Here’s what was genuinely difficult:</p>

<p><strong>Schema design is API design.</strong> When your schema drives UI, every property name, every <code class="language-plaintext highlighter-rouge">title</code>, every <code class="language-plaintext highlighter-rouge">description</code> matters. You’re designing a user interface in JSON. It requires the same care as designing a REST API, but the feedback loop is different – you only see problems when you render the form.</p>

<p><strong>UI layout schemas are necessary and annoying.</strong> Pure JSON Schema doesn’t encode layout. You need a separate UI schema to specify: which fields go in which section, what order, what hints to give the renderer. I use a <code class="language-plaintext highlighter-rouge">Categorization &gt; Category &gt; Control</code> hierarchy. It works, but it’s another artifact to maintain per entity type.</p>

<p><strong>Dynamic behavior requires schema extensions.</strong> Standard JSON Schema doesn’t handle “show this field only if that setting is enabled” or “populate this dropdown from a database query.” I ended up defining custom extensions: <code class="language-plaintext highlighter-rouge">x-options</code> for dynamic dropdowns (resolved server-side), <code class="language-plaintext highlighter-rouge">x-condition</code> for conditional visibility, <code class="language-plaintext highlighter-rouge">x-hidden</code> for fields present in data but invisible in forms. These extensions are processed both server-side (before delivery) and client-side (during rendering).</p>

<p><strong>Generic list views need explicit formatting.</strong> A schema tells you a field is a <code class="language-plaintext highlighter-rouge">number</code>, but not that it’s currency. List configurations need a <code class="language-plaintext highlighter-rouge">kind</code> property (<code class="language-plaintext highlighter-rouge">text</code>, <code class="language-plaintext highlighter-rouge">currency</code>, <code class="language-plaintext highlighter-rouge">date</code>, <code class="language-plaintext highlighter-rouge">boolean</code>) to format cells correctly. This is domain knowledge that JSON Schema alone can’t express.</p>

<h2 id="where-ai-fits-in">Where AI Fits In</h2>

<p>Here’s where it gets interesting. Once your entire domain model is expressed as schemas, <strong>AI can generate new domain models</strong>. A language model that understands your meta-schema can produce valid capability definitions from a natural language description.</p>

<p>“I run a yoga studio with class bookings and student management” becomes a complete set of resource types, forms, and settings – validated against the same meta-schemas, rendered by the same generic UI.</p>

<p>This only works <em>because</em> the target is structured data, not code. The AI generates configuration that’s mechanically validated before it touches production. The blast radius of a bad generation is a validation error, not a runtime crash.</p>

<p>I’ll write more about this in a follow-up post.</p>

<h2 id="lessons-for-staff-engineers">Lessons for Staff+ Engineers</h2>

<p>If you’re evaluating this pattern for your own platform:</p>

<p><strong>Start with the meta-schema.</strong> Before writing any application code, define the schema that validates your schemas. This forces you to think about what abstractions your platform actually needs. My meta-schema went through multiple major revisions before it stabilized, and each revision clarified the platform’s actual capabilities.</p>

<p><strong>Invest in server-side schema processing.</strong> Don’t send raw schemas to the client and let the frontend figure out conditional visibility and dynamic options. Process schemas on the server, resolve options against real data, filter invisible fields, and deliver a “ready to render” package. This keeps the frontend simple and prevents data leaks.</p>

<p><strong>Accept that you’ll need extensions.</strong> Vanilla JSON Schema won’t be enough. Define your extensions explicitly, document them, and process them in a single pipeline. Don’t scatter <code class="language-plaintext highlighter-rouge">x-</code> handling across your codebase.</p>

<p><strong>Generic views are an investment, not a shortcut.</strong> Building truly generic list/form/detail views takes longer upfront than building one bespoke page. The payoff comes at entity type three or four, and then it compounds. By entity type ten, you’re adding new domain models in an afternoon.</p>

<p><strong>JSON Schema + JSONB + generic UI = a platform that scales by configuration, not code.</strong> That’s the real unlock. Not any single technology, but the discipline of keeping domain meaning out of your core and expressing it entirely in validated, renderable schemas.</p>

<h2 id="the-bigger-shift">The Bigger Shift</h2>

<p>Building this platform surfaced an uncomfortable truth that I think the profession hasn’t reckoned with: <strong>the future of software engineering looks more like writing constraints than writing implementations.</strong></p>

<p>Schema-driven development inverts the traditional value hierarchy. Engineers treat schemas as “just configuration” – boring, unglamorous, not “real engineering.” But in a schema-driven system, the schema is more valuable than any code that interprets it. Code is increasingly replaceable – by AI, by new frameworks, by rewrites. My schemas encode product decisions, business rules, and domain constraints that represent months of hard-won understanding. They’re the durable artifact. The platform code that renders and validates against them could be rewritten in a weekend.</p>

<p>As AI gets better at generating implementations within constraints, the engineering skill that compounds isn’t writing code – it’s defining the boundaries that make correct output <em>inevitable</em>. The architect who defines a rigorous meta-schema is making thousands of future implementation decisions in advance. That’s leverage.</p>

<p>Most engineers will resist this shift because writing constraints feels less creative than writing implementations. I resisted it too. But the most impactful thing I built this year wasn’t an application. It was a 1,600-line JSON Schema document that makes applications <em>unnecessary</em>.</p>

<hr />

<p><em>This is Part 1 of a three-part series on building AI-enabled SaaS platforms. Next up: <a href="/ai/architecture/2025/02/08/ai-generates-configuration-not-code.html">using generative AI to produce validated product configuration at onboarding time</a>.</em></p>]]></content><author><name>Peter Hrynkow</name></author><category term="ai" /><category term="architecture" /><summary type="html"><![CDATA[I’ve spent the last several months building a multi-tenant SaaS platform from scratch. The kind where every customer needs slightly different data models, different forms, different workflows. The kind that, done wrong, turns into an unmaintainable mess of if (tenant.type === 'yoga_studio') branches.]]></summary></entry><entry><title type="html">Why Use a JavaScript Framework</title><link href="https://peterhrynkow.com/javascript/2019/07/13/why-frameworks.html" rel="alternate" type="text/html" title="Why Use a JavaScript Framework" /><published>2019-07-13T00:00:00+00:00</published><updated>2019-07-13T00:00:00+00:00</updated><id>https://peterhrynkow.com/javascript/2019/07/13/why-frameworks</id><content type="html" xml:base="https://peterhrynkow.com/javascript/2019/07/13/why-frameworks.html"><![CDATA[<p>Recently, I was asked why using a framework or library like React, Angular, or Vue.js is better for building web apps than vanilla JavaScript. Here are the reasons I keep coming back to.</p>

<h3 id="frameworks-and-libraries-provide-battle-tested-solutions-to-common-problems">Frameworks and libraries provide battle-tested solutions to common problems</h3>

<p>Server-side rendering, templating, and routing are just a few challenges you’ll face when building an application. These problems aren’t new. Framework developers have spent thousands of hours solving them, so you don’t have to. Their solutions are battle-tested for performance, reliability, and browser compatibility. Why reinvent the wheel when you can stand on their shoulders and spend your time on features?</p>

<h3 id="frameworks-guide-best-practices-for-security">Frameworks guide best practices for security</h3>

<p>Many frameworks offer a layer of protection against cross-site scripting (XSS). For instance, React uses automatic escaping to <a href="https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml">prevent developers from rendering untrusted HTML in the DOM</a>. It’s not bulletproof, but it means a developer with no security background is less likely to shoot themselves in the foot.</p>

<p>With vanilla JavaScript, that layer doesn’t exist. One careless <code class="language-plaintext highlighter-rouge">innerHTML</code> and 💥 you’ve got a problem.</p>

<h3 id="frameworks-guide-best-practices-for-accessibility">Frameworks guide best practices for accessibility</h3>

<p>In addition to written guidelines, most frameworks provide tooling to improve accessibility. Create React App, for example, ships with an <a href="https://github.com/facebook/create-react-app/blob/master/packages/eslint-config-react-app/README.md#accessibility-checks">eslint config</a> that checks your code for common accessibility issues. It doesn’t guarantee AAA accessibility, but it nudges developers in the right direction.</p>

<p><a href="https://reactjs.org/docs/accessibility.html">React a11y Guidelines</a><br />
<a href="https://guides.emberjs.com/release/reference/accessibility-guide/">Ember a11y Guidelines</a></p>

<h3 id="frameworks-provide-a-common-lexicon">Frameworks provide a common lexicon</h3>

<p>After using a framework for a while, you start to speak its language. Components, props, state—these mean the same thing in any React project. Having a common lexicon improves communication and helps new team members ramp up quickly.</p>

<h3 id="frameworks-are-inevitable">Frameworks are inevitable</h3>

<p>There’s a tipping point in most software projects where, if you didn’t start with a framework, you begin building one by accident. As you add features, complexity creeps in and you start generalizing pieces of code. Keep doing that and you end up with something that looks like a framework but with more bugs and less documentation than an off-the-shelf option.</p>

<p>Think about the features you could have been building while you were busy debugging that DOM manipulation code. Using a framework lets you focus on delivering value instead of reinventing the wheel.</p>

<h3 id="frameworks-have-opinions">Frameworks have opinions</h3>

<p>There are so many ways to build a modern web application that starting with vanilla JavaScript can lead to decision paralysis. How should your files be organized? How should your build scripts be configured? Opinionated frameworks like Ember make these decisions for you. That way your team can focus on what’s most important: building a great product.</p>]]></content><author><name>Peter Hrynkow</name></author><category term="javascript" /><summary type="html"><![CDATA[Recently, I was asked why using a framework or library like React, Angular, or Vue.js is better for building web apps than vanilla JavaScript. Here are the reasons I keep coming back to.]]></summary></entry><entry><title type="html">Hacking Image Interpolation for Fun and Profit</title><link href="https://peterhrynkow.com/performance/2019/01/13/blowing-up-images-to-make-them-small.html" rel="alternate" type="text/html" title="Hacking Image Interpolation for Fun and Profit" /><published>2019-01-13T00:00:00+00:00</published><updated>2019-01-13T00:00:00+00:00</updated><id>https://peterhrynkow.com/performance/2019/01/13/blowing-up-images-to-make-them-small</id><content type="html" xml:base="https://peterhrynkow.com/performance/2019/01/13/blowing-up-images-to-make-them-small.html"><![CDATA[<p>Raster images normally look distorted or pixelated when enlarged—especially if they contain graphics or text. The technique below is about blowing up tiny images, and the results might surprise you.</p>

<p>Let’s say you want to use this image as a full page background on your website:</p>

<p><a href="/images/1920x1080.jpg">
  <img src="/images/1920x1080.jpg" />
</a>
<code class="language-plaintext highlighter-rouge">1920x1080.jpg (22 KB)</code></p>

<p>Using SVG or CSS gradients would be ideal for reducing the file size and providing resolution-independent scaling. Sadly, <a href="https://stackoverflow.com/questions/14926189/creating-a-gradient-mesh-in-css-jquery">neither technology supports mesh gradients</a> at this time.</p>

<p>Fortunately, there’s another way to achieve both a tiny file size and vector-like scaling using a raster image.</p>

<p>Here’s how it works:</p>
<ol>
  <li>Downsample the image so its dimensions are <code class="language-plaintext highlighter-rouge">32x18</code>.</li>
  <li>Export the image as a PNG. The resulting file <a href="/images/32x18.png"><img src="/images/32x18.png" /></a> should be around <code class="language-plaintext highlighter-rouge">1 KB</code>.</li>
  <li>Use the <code class="language-plaintext highlighter-rouge">32x18</code> image as a CSS background that covers its container:
    <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.my-background</span> <span class="p">{</span>
  <span class="nl">width</span><span class="p">:</span> <span class="m">1920px</span><span class="p">;</span>
  <span class="nl">height</span><span class="p">:</span> <span class="m">1080px</span><span class="p">;</span>
  <span class="nl">background</span><span class="p">:</span> <span class="sx">url(32x18.png)</span><span class="p">;</span>
  <span class="nl">background-size</span><span class="p">:</span> <span class="n">cover</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>    </div>
    <p>or as an <code class="language-plaintext highlighter-rouge">&lt;img&gt;</code> element:</p>
    <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"32x18.png"</span> <span class="na">style=</span><span class="s">"width: 100%; height: auto;"</span> <span class="nt">/&gt;</span>
</code></pre></div>    </div>
  </li>
</ol>

<p>Here is the result:</p>

<p><a href="/images/32x18.png">
  <img src="/images/32x18.png" style="width: 100%; height auto;" />
</a>
<code class="language-plaintext highlighter-rouge">32x18.png (1443 bytes)</code></p>

<p>Can you tell the difference? The stretched image looks almost identical to the original. It scales up infinitely without noticeable distortion. Better yet, the new image is only <code class="language-plaintext highlighter-rouge">1.4KB</code>—a 93.6% reduction 🎉. <a href="https://codepen.io/peterhry/pen/maaXZX">Click here</a> for a side-by-side comparison.</p>

<p>Here is another example:</p>

<p><a href="/images/1920x1080-2.jpg">
  <img src="/images/1920x1080-2.jpg" />
</a>
<code class="language-plaintext highlighter-rouge">1920x1080-2.jpg (143 KB)</code></p>

<p>This image has more detail than the previous one. In this case, the small image needs to be a bit larger <code class="language-plaintext highlighter-rouge">(128x72)</code> to capture the detail. Finding the right size takes some experimenting.</p>

<p><a href="/images/128x72.png">
  <img src="/images/128x72.png" style="width: 100%; height auto;" />
</a>
<code class="language-plaintext highlighter-rouge">128x72.png (7 KB)</code></p>

<p>This technique is also suitable for blurred background photos—the kind you typically see behind a text overlay. In this case, saving the image as a JPEG yields a smaller file size.</p>

<p><a href="/images/1920x1080-4.jpg">
  <img src="/images/1920x1080-4.jpg" />
</a>
<code class="language-plaintext highlighter-rouge">1920x1080-4.jpg (199 KB)</code></p>

<p><a href="/images/256x144-4.jpg">
  <img src="/images/256x144-4.jpg" style="width: 100%; height auto;" />
</a>
<code class="language-plaintext highlighter-rouge">256x144-4.jpg (10 KB)</code></p>

<h3 id="limitations">Limitations</h3>

<p>Before you get too excited, this technique does have one limitation. It works well for smooth gradients and images with less contrast, but look what happens if you blow up this image:</p>

<p><img src="/images/1920x1080-3.jpg" />
<code class="language-plaintext highlighter-rouge">1920x1080-3.jpg (337 KB)</code></p>

<p><img src="/images/128x72-2.png" style="width: 100%; height auto;" />
<code class="language-plaintext highlighter-rouge">128x72-2.png (21 KB)</code></p>

<h3 id="whats-going-on-here">What’s going on here?</h3>

<p>When the downsampled image is enlarged, the browser uses an <a href="https://en.wikipedia.org/wiki/Image_scaling#Algorithms">interpolation algorithm</a> to fill in the missing data. Smooth images with less contrast can be upsampled without noticeable distortion because the interpolated pixels blend in with the originals.</p>

<h3 id="how-is-this-useful">How is this useful?</h3>

<p>This technique is great for reducing the file size of background images that often contain less detail. It’s also great for rendering lightweight, scalable mesh gradients—something you can’t currently do with SVG or CSS. Mesh gradients were planned for SVG 2.0 but <a href="http://libregraphicsworld.org/blog/entry/gradient-meshes-and-hatching-to-be-removed-from-svg-2-0">the feature has since been removed</a> from the spec.</p>

<p>So the next time you’re about to export a big background image from Photoshop or Sketch, give this technique a try. You might be surprised how well it works.</p>]]></content><author><name>Peter Hrynkow</name></author><category term="performance" /><summary type="html"><![CDATA[Raster images normally look distorted or pixelated when enlarged—especially if they contain graphics or text. The technique below is about blowing up tiny images, and the results might surprise you.]]></summary></entry><entry><title type="html">The Perils of Jest Snapshot Testing</title><link href="https://peterhrynkow.com/testing/2019/01/07/the-perils-of-snapshot-testing.html" rel="alternate" type="text/html" title="The Perils of Jest Snapshot Testing" /><published>2019-01-07T00:00:00+00:00</published><updated>2019-01-07T00:00:00+00:00</updated><id>https://peterhrynkow.com/testing/2019/01/07/the-perils-of-snapshot-testing</id><content type="html" xml:base="https://peterhrynkow.com/testing/2019/01/07/the-perils-of-snapshot-testing.html"><![CDATA[<p>According to the <a href="https://jestjs.io/docs/en/snapshot-testing">Jest docs</a>, snapshot tests help ensure your UI doesn’t change unexpectedly. That sounds great, but in practice snapshot tests can create more noise than signal. Here’s why.</p>

<p>Say you have a simple button component:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><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="nt">a</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="p">&gt;</span><span class="si">{</span><span class="nx">children</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>
<span class="p">)</span>
</code></pre></div></div>

<p>You can create a snapshot test for it like so:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">Button</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">renderer</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-test-renderer</span><span class="dl">'</span>

<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">renders correctly</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">tree</span> <span class="o">=</span> <span class="nx">renderer</span>
    <span class="p">.</span><span class="nx">create</span><span class="p">(&lt;</span><span class="nc">Button</span> <span class="na">href</span><span class="p">=</span><span class="s">"https://myurl.com"</span><span class="p">&gt;</span>My Label<span class="p">&lt;/</span><span class="nc">Button</span><span class="p">&gt;)</span>
    <span class="p">.</span><span class="nx">toJSON</span><span class="p">()</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">tree</span><span class="p">).</span><span class="nx">toMatchSnapshot</span><span class="p">()</span>
<span class="p">})</span>
</code></pre></div></div>

<p>Now any change to the component’s rendered output will cause the test to fail. Sounds great, right?</p>

<p>Consider what happens when you add a new attribute <code class="language-plaintext highlighter-rouge">target</code> but mistype it as <code class="language-plaintext highlighter-rouge">traget</code>:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><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">target</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="nt">a</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">traget</span><span class="p">=</span><span class="si">{</span><span class="nx">target</span><span class="si">}</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="nt">a</span><span class="p">&gt;</span>
<span class="p">)</span>
</code></pre></div></div>

<p>You expect the test to fail, since the rendered output changed. In haste you skim the diff, miss the typo, and update the snapshot anyway. Now you have a passing test that asserts the wrong output. WTF just happened?</p>

<h3 id="sorting-the-catch">Sorting the Catch</h3>

<p>When a snapshot test fails, you must review each change and decide whether it’s a bug or a valid update. That review is tedious and error-prone, especially when lots of changes pile up.</p>

<p><img src="/images/fish.jpg" alt="Files" /></p>

<p>Snapshot testing is like fishing with a giant net. You want bugs, but you also catch plenty of valid changes. Sorting the catch—deciding what to keep and what to throw back—is the hard part. The risk is updating a snapshot because you assume the change is correct, when in fact it’s a bug.</p>

<p>Things get worse with <a href="https://github.com/styled-components/jest-styled-components">jest-styled-components</a>, which stores style rules with the snapshot. Now you’re reviewing every line of changing CSS, too.</p>

<h3 id="snapshot-fatigue">Snapshot Fatigue</h3>

<p>Snapshot tests are easy to create because they push all the cognitive load to the reviewer. After a while engineers experience <em>snapshot fatigue</em> and start blindly updating failed snapshots without reviewing them. At that point the tests are useless.</p>

<h3 id="do-you-even-need-a-snapshot">Do You Even Need a Snapshot?</h3>

<p>A good test prevents you from accidentally breaking a component’s API. By creating a snapshot test, you’re essentially declaring that the component’s <em>entire</em> rendered output is part of its API and should never change. Sometimes that’s true, but usually a component has specific behaviors you care about. Freezing its entire output makes refactoring painful.</p>

<p>Instead, identify the UI elements that are critical to behavior and test those explicitly.</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">Button</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">render</span><span class="p">,</span> <span class="nx">getByText</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-testing-library</span><span class="dl">'</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">react-testing-library/cleanup-after-each</span><span class="dl">'</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">jest-dom/extend-expect</span><span class="dl">'</span>

<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">renders correctly</span><span class="dl">'</span><span class="p">,</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">container</span><span class="p">}</span> <span class="o">=</span> <span class="nx">render</span><span class="p">(&lt;</span><span class="nc">Button</span> <span class="na">href</span><span class="p">=</span><span class="s">"https://myurl.com"</span> <span class="na">target</span><span class="p">=</span><span class="s">"_blank"</span><span class="p">&gt;</span>My Label<span class="p">&lt;/</span><span class="nc">Button</span><span class="p">&gt;)</span>
  <span class="kd">const</span> <span class="nx">button</span> <span class="o">=</span> <span class="nx">getByText</span><span class="p">(</span><span class="nx">container</span><span class="p">,</span> <span class="dl">'</span><span class="s1">My Label</span><span class="dl">'</span><span class="p">)</span>

  <span class="nx">expect</span><span class="p">(</span><span class="nx">button</span><span class="p">).</span><span class="nx">toHaveAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">target</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">_blank</span><span class="dl">'</span><span class="p">)</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">button</span><span class="p">).</span><span class="nx">toHaveAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">href</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">https://myurl.com</span><span class="dl">'</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div></div>

<p>This test is explicit. It verifies that the link contains the correct text and that its <code class="language-plaintext highlighter-rouge">href</code> and <code class="language-plaintext highlighter-rouge">target</code> values are correct. As long as those values don’t change, the component should function as expected.</p>

<p>The method requires more thought upfront but eliminates the burden and risk of manually reviewing snapshot diffs over time. It also lets you refactor freely as long as those specific values stay intact.</p>]]></content><author><name>Peter Hrynkow</name></author><category term="testing" /><summary type="html"><![CDATA[According to the Jest docs, snapshot tests help ensure your UI doesn’t change unexpectedly. That sounds great, but in practice snapshot tests can create more noise than signal. Here’s why.]]></summary></entry><entry><title type="html">‪A Better Way to Test Your React-Redux App‬</title><link href="https://peterhrynkow.com/testing/2019/01/01/testing-atoms-and-molecules.html" rel="alternate" type="text/html" title="‪A Better Way to Test Your React-Redux App‬" /><published>2019-01-01T00:00:00+00:00</published><updated>2019-01-01T00:00:00+00:00</updated><id>https://peterhrynkow.com/testing/2019/01/01/testing-atoms-and-molecules</id><content type="html" xml:base="https://peterhrynkow.com/testing/2019/01/01/testing-atoms-and-molecules.html"><![CDATA[<p><strong>Update:</strong> The approach described in this post is now the <a href="https://redux.js.org/usage/writing-tests">recommended way to test</a> in the official Redux docs.</p>

<p>I come across a lot of React-Redux apps where components, action creators, selectors, and reducers are tested as separate units. It’s a common practice—it’s even described in the <a href="https://web.archive.org/web/20190108083439/https://redux.js.org/recipes/writing-tests">Redux docs</a> (archived)—but there’s a better way.</p>

<p>In this post, I’ll show you why the standard approach for testing React-Redux apps is insufficient and makes refactoring harder. I’ll also show you an easier way to test your app that catches more bugs and keeps refactors safe.</p>

<h3 id="testing-atoms">Testing Atoms</h3>

<p>So what’s the problem with testing components, action creators, selectors, and reducers separately?</p>

<p>First, testing these elements in isolation doesn’t guarantee that they work together. A unit test for an <a href="https://web.archive.org/web/20190108083439/https://redux.js.org/recipes/writing-tests#action-creators">action creator</a> asserts that an action is created but doesn’t verify that the action is ever dispatched. A unit test for a <a href="https://web.archive.org/web/20190108083439/https://redux.js.org/recipes/writing-tests#reducers">reducer</a> asserts that a new state is returned but doesn’t verify that the UI updates to reflect it. There’s a disconnect.</p>

<p><img src="/images/unit-tests.jpg" alt="Passing unit tests, sinking ship" /></p>

<p>Second, because these tests require you to mock other parts of the system, you lose confidence in the integration between what you’re testing and the dependency being mocked. For example, the Redux docs recommend <a href="https://github.com/dmitry-zaets/redux-mock-store">redux-mock-store</a> for async action creators. A mock store <em>looks</em> like a real Redux store, but its state is static. It lets you verify that certain actions are dispatched but tells you nothing about how those actions change real state.</p>

<p>Finally, these tests are so granular that refactoring becomes painful. A small change to one module often requires updates to several tests. This slows development and increases the chance of new bugs.</p>

<p>That’s what I call testing the <em>atoms</em>. Knowing that tiny chunks of code work in isolation is great, but to be confident they work together, test the <em>molecules</em>.</p>

<h3 id="testing-molecules">Testing Molecules</h3>

<p>Components, action creators, selectors, and reducers are like atoms that combine to create a connected component <em>molecule</em>. Testing the molecule verifies the connections between its atoms.</p>

<p>Here’s an example:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Component (atom)</span>
<span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span>

<span class="kd">const</span> <span class="nx">Counter</span> <span class="o">=</span> <span class="p">({</span><span class="nx">count</span><span class="p">,</span> <span class="nx">onClick</span><span class="p">})</span> <span class="o">=&gt;</span> <span class="p">(</span>
  <span class="p">&lt;</span><span class="nt">button</span> <span class="na">title</span><span class="p">=</span><span class="s">"Click Me"</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">onClick</span><span class="si">}</span><span class="p">&gt;</span>Count: <span class="si">{</span><span class="nx">count</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
<span class="p">)</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">Counter</span>
</code></pre></div></div>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Action creator (atom)</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">incrementCounter</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">INCREMENT_COUNTER</span><span class="dl">'</span>
<span class="p">})</span>
</code></pre></div></div>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Reducer (atom)</span>
<span class="kd">const</span> <span class="nx">counterReducer</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span> <span class="o">=</span> <span class="p">{</span><span class="na">count</span><span class="p">:</span> <span class="mi">0</span><span class="p">},</span> <span class="nx">action</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="dl">'</span><span class="s1">INCREMENT_COUNTER</span><span class="dl">'</span><span class="p">:</span>
      <span class="k">return</span> <span class="p">{</span>
        <span class="na">count</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span>
      <span class="p">}</span>
    <span class="nl">default</span><span class="p">:</span>
      <span class="k">return</span> <span class="nx">state</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">counterReducer</span>
</code></pre></div></div>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Connected component (molecule)</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">connect</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-redux</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">incrementCounter</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">/actions</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">Counter</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./counter</span><span class="dl">'</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">connect</span><span class="p">(</span>
  <span class="p">(</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">state</span><span class="p">,</span>
  <span class="p">{</span>
    <span class="na">onClick</span><span class="p">:</span> <span class="nx">incrementCounter</span>
  <span class="p">}</span>
<span class="p">)(</span><span class="nx">Counter</span><span class="p">)</span>
</code></pre></div></div>

<p>The component renders a button that increments a counter when clicked.</p>

<p>Here’s what the molecule test looks like:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">createStore</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">redux</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">Provider</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-redux</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span><span class="nx">render</span><span class="p">,</span> <span class="nx">getByTitle</span><span class="p">,</span> <span class="nx">fireEvent</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-testing-library</span><span class="dl">'</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">react-testing-library/cleanup-after-each</span><span class="dl">'</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">jest-dom/extend-expect</span><span class="dl">'</span>

<span class="k">import</span> <span class="nx">Counter</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">counterReducer</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">/reducers/counter-reducer</span><span class="dl">'</span>

<span class="c1">// Create a real redux store</span>
<span class="kd">const</span> <span class="nx">store</span> <span class="o">=</span> <span class="nx">createStore</span><span class="p">(</span><span class="nx">counterReducer</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">count</span><span class="p">:</span> <span class="mi">0</span>
<span class="p">})</span>

<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">increments the counter</span><span class="dl">'</span><span class="p">,</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">container</span><span class="p">}</span> <span class="o">=</span> <span class="nx">render</span><span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">Provider</span> <span class="na">store</span><span class="p">=</span><span class="si">{</span><span class="nx">store</span><span class="si">}</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">Counter</span> <span class="p">/&gt;</span>
    <span class="p">&lt;/</span><span class="nc">Provider</span><span class="p">&gt;</span>
  <span class="p">)</span>

  <span class="kd">const</span> <span class="nx">button</span> <span class="o">=</span> <span class="nx">getByTitle</span><span class="p">(</span><span class="nx">container</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Click Me</span><span class="dl">'</span><span class="p">)</span>

  <span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">button</span><span class="p">)</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">button</span><span class="p">).</span><span class="nx">toHaveTextContent</span><span class="p">(</span><span class="dl">'</span><span class="s1">Count: 1</span><span class="dl">'</span><span class="p">)</span>

  <span class="nx">fireEvent</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">button</span><span class="p">)</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">button</span><span class="p">).</span><span class="nx">toHaveTextContent</span><span class="p">(</span><span class="dl">'</span><span class="s1">Count: 2</span><span class="dl">'</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div></div>

<p>A few takeaways:</p>

<ul>
  <li>The connected component is the only module under test, but the test covers every line inside the other modules. <strong>By testing a molecule, you indirectly test its atoms.</strong></li>
  <li>The test verifies the connections between atoms. It fails if the component’s <code class="language-plaintext highlighter-rouge">onClick</code> prop isn’t wired to <code class="language-plaintext highlighter-rouge">incrementCounter</code> or if the reducer mishandles the <code class="language-plaintext highlighter-rouge">INCREMENT_COUNTER</code> action.</li>
  <li>The test uses a real Redux store instead of <a href="https://github.com/dmitry-zaets/redux-mock-store">redux-mock-store</a>. <strong>Using a real store closes the loop between UI event (input) and UI update (output).</strong> You fire an event and assert that the UI updates. With a mock store, you can only assert that actions were dispatched.</li>
</ul>

<p>Because the test focuses on behavior rather than implementation details, you can refactor the atoms without breaking it.</p>

<blockquote>
  <p>Many people assume integration tests are necessarily broad in scope, while they can be more effectively done with a narrower scope.</p>
</blockquote>

<p><a href="https://twitter.com/martinfowler">@martinfowler</a></p>

<p>Narrow integration tests give you more confidence in the stability of your application because they verify the connections between smaller units of code. Instead of writing a unit test for every atom in your app, zoom out and write integration tests for the molecules.</p>

<blockquote>
  <p>Write tests. Not too many. Mostly integration.</p>
</blockquote>

<p><a href="https://twitter.com/rauchg">@rauchg</a></p>

<p>There are plenty of scenarios where unit tests make sense (shared libraries, TDD, etc.) but for testing the overall behaviour of your application, integration tests are more likely to catch problems.</p>

<h3 id="summary">Summary</h3>

<ul>
  <li>Unit tests don’t cover the connections between components, action creators, selectors, and reducers.</li>
  <li>Integration tests give you more confidence in the stability of your application because they verify the relationships between units of code.</li>
  <li>Testing a connected component from the UI allows you to refactor its implementation freely.</li>
  <li>Integration tests don’t have to be wide in scope. You can use them to test the connection between just a handful of modules.</li>
</ul>]]></content><author><name>Peter Hrynkow</name></author><category term="testing" /><summary type="html"><![CDATA[Update: The approach described in this post is now the recommended way to test in the official Redux docs.]]></summary></entry><entry><title type="html">Firebase + Create React App</title><link href="https://peterhrynkow.com/firebase/2018/08/01/firebase-with-create-react-app.html" rel="alternate" type="text/html" title="Firebase + Create React App" /><published>2018-08-01T00:00:00+00:00</published><updated>2018-08-01T00:00:00+00:00</updated><id>https://peterhrynkow.com/firebase/2018/08/01/firebase-with-create-react-app</id><content type="html" xml:base="https://peterhrynkow.com/firebase/2018/08/01/firebase-with-create-react-app.html"><![CDATA[<p>Earlier this year my team built and shipped its first production Firebase app. We needed a web-based chat client where users could interact with market research bots in real time. Firebase was the perfect fit: the Realtime Database kept clients in sync, Cloud Functions processed messages, and Firebase Hosting served the app.</p>

<p>On the client side we chose <a href="https://github.com/facebook/create-react-app">Create React App</a> for its simple, batteries-included tooling. Combining the two is straightforward, but getting local development right can be tricky. Here’s the recipe that worked for us.</p>

<h3 id="dev-servers">Dev servers</h3>

<p>If you plan to use Firebase Functions and Firebase Hosting, run the Create React App dev server <strong>alongside</strong> the Firebase emulator. That way you can emulate HTTP functions and hosting while your React app runs in dev mode.</p>

<p>Install <code class="language-plaintext highlighter-rouge">npm-run-all</code> to orchestrate both servers:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add <span class="nt">--dev</span> npm-run-all
</code></pre></div></div>

<p>Add these scripts to <code class="language-plaintext highlighter-rouge">package.json</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="dl">"</span><span class="s2">scripts</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">dev</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">run-p --race dev:firebase dev:react</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">dev:firebase</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">firebase serve -p 4000</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">dev:react</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">react-scripts start</span><span class="dl">"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Run <code class="language-plaintext highlighter-rouge">yarn dev</code> to start both. React stays on <a href="http://localhost:3000">http://localhost:3000</a> while the Firebase project is emulated on port 4000. The <code class="language-plaintext highlighter-rouge">--race</code> flag ensures either process exiting will stop the other so you don’t end up with orphaned servers.</p>

<h3 id="implicit-initialization">Implicit initialization</h3>

<p>When deploying to Firebase Hosting, use <a href="https://firebase.google.com/docs/web/setup#sdk_imports_and_implicit_initialization">implicit initialization</a>. It removes the need to juggle environment-specific configs.</p>

<p>Instead of manually configuring each environment:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script</span> <span class="na">src=</span><span class="s">"https://www.gstatic.com/firebasejs/5.3.0/firebase.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script&gt;</span>
  firebase.initializeApp({
    apiKey: "<span class="nt">&lt;API_KEY&gt;</span>",
    authDomain: "<span class="nt">&lt;PROJECT_ID&gt;</span>.firebaseapp.com",
    databaseURL: "https://<span class="nt">&lt;DATABASE_NAME&gt;</span>.firebaseio.com",
    projectId: "<span class="nt">&lt;PROJECT_ID&gt;</span>",
    storageBucket: "<span class="nt">&lt;BUCKET&gt;</span>.appspot.com",
    messagingSenderId: "<span class="nt">&lt;SENDER_ID&gt;</span>",
  })
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>Just load the auto-configured scripts from Firebase Hosting:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script</span> <span class="na">src=</span><span class="s">"/__/firebase/5.3.0/firebase.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script</span> <span class="na">src=</span><span class="s">"/__/firebase/init.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>Once deployed, the app is automatically configured for its host project.</p>

<p>You might be wondering about that <code class="language-plaintext highlighter-rouge">/__</code> directory. Because it only exists in the Firebase Hosting environment, tell the React dev server how to proxy those requests locally by adding this to <code class="language-plaintext highlighter-rouge">package.json</code>:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="dl">"</span><span class="s2">proxy</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:4000</span><span class="dl">"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <a href="https://create-react-app.dev/docs/proxying-api-requests-in-development/">proxy</a> setting sends unknown requests to the target URL. Any request containing <code class="language-plaintext highlighter-rouge">/__</code> now reaches the Firebase Hosting emulator running on port 4000.</p>

<h3 id="calling-firebase-functions-from-your-app">Calling Firebase Functions from your app</h3>

<p>If you call an HTTP Firebase Function from React, you might be tempted to store environment-specific URLs. A cleaner option is to configure a rewrite in <code class="language-plaintext highlighter-rouge">firebase.json</code> that exposes the function behind a friendly path:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="dl">"</span><span class="s2">hosting</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">public</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">build</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">ignore</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">firebase.json</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">**/.*</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">**/node_modules/**</span><span class="dl">"</span><span class="p">],</span>
    <span class="dl">"</span><span class="s2">rewrites</span><span class="dl">"</span><span class="p">:</span> <span class="p">[</span>
      <span class="p">{</span>
        <span class="dl">"</span><span class="s2">source</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/myFunction</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// Some URL to expose</span>
        <span class="dl">"</span><span class="s2">function</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">myFunction</span><span class="dl">"</span> <span class="c1">// Your HTTP function</span>
      <span class="p">},</span>
      <span class="p">{</span>
        <span class="dl">"</span><span class="s2">source</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">**</span><span class="dl">"</span><span class="p">,</span>
        <span class="dl">"</span><span class="s2">destination</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/index.html</span><span class="dl">"</span>
      <span class="p">}</span>
    <span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now your app can call <code class="language-plaintext highlighter-rouge">/myFunction</code> and Firebase Hosting will route it to the correct backend.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">myData</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/myFunction</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div></div>

<p>To make that rewrite work locally, expand the proxy entry in <code class="language-plaintext highlighter-rouge">package.json</code>:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="dl">"</span><span class="s2">proxy</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">/__</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
      <span class="dl">"</span><span class="s2">target</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:4000</span><span class="dl">"</span>
    <span class="p">}</span>
    <span class="dl">"</span><span class="s2">/myFunction</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
      <span class="dl">"</span><span class="s2">target</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:4000</span><span class="dl">"</span>
    <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>This approach works, but if you plan to add more functions, a scalable version might look like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="dl">"</span><span class="s2">proxy</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">/__</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
      <span class="dl">"</span><span class="s2">target</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:4000</span><span class="dl">"</span>
    <span class="p">}</span>
    <span class="dl">"</span><span class="s2">/functions</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
      <span class="dl">"</span><span class="s2">target</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">http://localhost:4000</span><span class="dl">"</span>
    <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>Any request starting with <code class="language-plaintext highlighter-rouge">/functions</code> now reaches the emulator, keeping local development predictable while matching production routing.</p>]]></content><author><name>Peter Hrynkow</name></author><category term="firebase" /><summary type="html"><![CDATA[Earlier this year my team built and shipped its first production Firebase app. We needed a web-based chat client where users could interact with market research bots in real time. Firebase was the perfect fit: the Realtime Database kept clients in sync, Cloud Functions processed messages, and Firebase Hosting served the app.]]></summary></entry><entry><title type="html">Partial Application in Action</title><link href="https://peterhrynkow.com/functional-programming/2017/09/29/using-function-bind-for-partial-application.html" rel="alternate" type="text/html" title="Partial Application in Action" /><published>2017-09-29T00:00:00+00:00</published><updated>2017-09-29T00:00:00+00:00</updated><id>https://peterhrynkow.com/functional-programming/2017/09/29/using-function-bind-for-partial-application</id><content type="html" xml:base="https://peterhrynkow.com/functional-programming/2017/09/29/using-function-bind-for-partial-application.html"><![CDATA[<p>Have you ever called the same function with the same argument over and over again? There’s a simple technique that reduces this repetition: <a href="https://en.wikipedia.org/wiki/Partial_application">partial application</a>.</p>

<p>Say you have a function that adds two numbers:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">add</span> <span class="o">=</span> <span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span>

<span class="nx">add</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span>
<span class="c1">// 6</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Function.prototype.bind</code> lets you partially apply a function by fixing one or more arguments.</p>

<p>For example, if you partially apply <code class="language-plaintext highlighter-rouge">add</code> and fix the first argument with <code class="language-plaintext highlighter-rouge">10</code>, you get a new function <code class="language-plaintext highlighter-rouge">addTen</code> that adds <code class="language-plaintext highlighter-rouge">10</code> to whatever you pass in.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">add</span> <span class="o">=</span> <span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span>
<span class="kd">const</span> <span class="nx">addTen</span> <span class="o">=</span> <span class="nx">add</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span>

<span class="nx">addTen</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
<span class="c1">// 13</span>

<span class="nx">addTen</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span>
<span class="c1">// 16</span>
</code></pre></div></div>

<p>That’s a common demo, but here’s a practical use.</p>

<h3 id="reducing-argument-repetition">Reducing argument repetition</h3>

<p>If you want to fetch several resources from an API, your code might look like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">apiBaseUrl</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">https://api.myapp.com/api/v1</span><span class="dl">'</span>
<span class="kd">const</span> <span class="nx">getData</span> <span class="o">=</span> <span class="p">(</span><span class="nx">baseUrl</span><span class="p">,</span> <span class="nx">resource</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">baseUrl</span><span class="p">}</span><span class="s2">/</span><span class="p">${</span><span class="nx">resource</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>

<span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">([</span>
  <span class="nx">getData</span><span class="p">(</span><span class="nx">apiBaseUrl</span><span class="p">,</span> <span class="dl">'</span><span class="s1">products</span><span class="dl">'</span><span class="p">),</span>
  <span class="nx">getData</span><span class="p">(</span><span class="nx">apiBaseUrl</span><span class="p">,</span> <span class="dl">'</span><span class="s1">categories</span><span class="dl">'</span><span class="p">),</span>
  <span class="nx">getData</span><span class="p">(</span><span class="nx">apiBaseUrl</span><span class="p">,</span> <span class="dl">'</span><span class="s1">tags</span><span class="dl">'</span><span class="p">),</span>
<span class="p">]).</span><span class="nx">then</span><span class="p">((</span><span class="nx">responses</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// Do stuff</span>
<span class="p">})</span>
</code></pre></div></div>

<p>Notice that <code class="language-plaintext highlighter-rouge">getData</code> and the first argument <code class="language-plaintext highlighter-rouge">apiBaseUrl</code> repeat. You can partially apply <code class="language-plaintext highlighter-rouge">getData</code> to remove that duplication.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">apiBaseUrl</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">https://api.myapp.com/api/v1</span><span class="dl">'</span>
<span class="kd">const</span> <span class="nx">getData</span> <span class="o">=</span> <span class="p">(</span><span class="nx">baseUrl</span><span class="p">,</span> <span class="nx">resource</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">baseUrl</span><span class="p">}</span><span class="s2">/</span><span class="p">${</span><span class="nx">resource</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">getDataFromAPI</span> <span class="o">=</span> <span class="nx">getData</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">apiBaseUrl</span><span class="p">)</span>

<span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">([</span>
  <span class="nx">getDataFromAPI</span><span class="p">(</span><span class="dl">'</span><span class="s1">products</span><span class="dl">'</span><span class="p">),</span>
  <span class="nx">getDataFromAPI</span><span class="p">(</span><span class="dl">'</span><span class="s1">categories</span><span class="dl">'</span><span class="p">),</span>
  <span class="nx">getDataFromAPI</span><span class="p">(</span><span class="dl">'</span><span class="s1">tags</span><span class="dl">'</span><span class="p">),</span>
<span class="p">]).</span><span class="nx">then</span><span class="p">((</span><span class="nx">responses</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// Do stuff</span>
<span class="p">})</span>
</code></pre></div></div>

<p>That’s better: the base URL is declared once, and every call shares it.</p>

<h3 id="reducing-function-call-repetition">Reducing function call repetition</h3>

<p>Because <code class="language-plaintext highlighter-rouge">getDataFromAPI</code> now takes a single argument, you can pass it directly to <code class="language-plaintext highlighter-rouge">map</code>.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">apiBaseUrl</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">https://api.myapp.com/api/v1</span><span class="dl">'</span>
<span class="kd">const</span> <span class="nx">getData</span> <span class="o">=</span> <span class="p">(</span><span class="nx">baseUrl</span><span class="p">,</span> <span class="nx">path</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">baseUrl</span><span class="p">}</span><span class="s2">/</span><span class="p">${</span><span class="nx">path</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">getDataFromAPI</span> <span class="o">=</span> <span class="nx">getData</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">apiBaseUrl</span><span class="p">)</span>

<span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">([</span><span class="dl">'</span><span class="s1">products</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">categories</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">tags</span><span class="dl">'</span><span class="p">].</span><span class="nx">map</span><span class="p">(</span><span class="nx">getDataFromAPI</span><span class="p">)).</span><span class="nx">then</span><span class="p">(</span>
  <span class="p">(</span><span class="nx">responses</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// Do stuff</span>
  <span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<p>No more repetition—and the intent is clearer. Partial application is a tiny tool, but once you start using it your code often becomes more expressive and easier to refactor.</p>]]></content><author><name>Peter Hrynkow</name></author><category term="functional-programming" /><summary type="html"><![CDATA[Have you ever called the same function with the same argument over and over again? There’s a simple technique that reduces this repetition: partial application.]]></summary></entry><entry><title type="html">Using SVG to Shrink Your PNGs</title><link href="https://peterhrynkow.com/how-to-compress-a-png-like-a-jpeg/" rel="alternate" type="text/html" title="Using SVG to Shrink Your PNGs" /><published>2014-09-07T00:00:00+00:00</published><updated>2014-09-07T00:00:00+00:00</updated><id>https://peterhrynkow.com/using-svg-to-shrink-your-pngs</id><content type="html" xml:base="https://peterhrynkow.com/how-to-compress-a-png-like-a-jpeg/"><![CDATA[<p>Wouldn’t it be great if you could get the compression of a JPEG with the transparency of a PNG? You can, with a little layering trick I used while working on the new<a href="http://sapporobeer.ca" target="_blank"> Sapporo Beer website.</a></p>

<p><img src="/images/sapporo.jpg" alt="Saporro Beer" /></p>

<p>Notice how the beer can has a transparent area around the edges (there’s a video playing behind it). As a single PNG, the can graphic weighed in at over 1.2 MB. The trick below brought it down to 271 KB without giving up transparency.</p>

<p>First, create two files. The first is a regular JPEG without any transparency; you can compress it aggressively. The second is a tiny 8-bit PNG (alpha mask) that represents the transparent areas. The PNG is only 11 KB because it contains so few colors and no blended transparency.</p>

<p><img src="/images/2files1.jpg" alt="Files" /></p>

<p>Next, I created a little snippet of inline SVG:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;svg</span> <span class="na">viewBox=</span><span class="s">"0 0 560 1388"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;defs&gt;</span>
    <span class="nt">&lt;mask</span> <span class="na">id=</span><span class="s">"canTopMask"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;image</span>
        <span class="na">width=</span><span class="s">"560"</span>
        <span class="na">height=</span><span class="s">"1388"</span>
        <span class="na">xlink:href=</span><span class="s">"img/can-top-alpha.png"</span><span class="nt">&gt;&lt;/image&gt;</span>
    <span class="nt">&lt;/mask&gt;</span>
  <span class="nt">&lt;/defs&gt;</span>
  <span class="nt">&lt;image</span>
    <span class="na">mask=</span><span class="s">"url(#canTopMask)"</span>
    <span class="na">id=</span><span class="s">"canTop"</span>
    <span class="na">width=</span><span class="s">"560"</span>
    <span class="na">height=</span><span class="s">"1388"</span>
    <span class="na">xlink:href=</span><span class="s">"can-top.jpg"</span><span class="nt">&gt;&lt;/image&gt;</span>
<span class="nt">&lt;/svg&gt;</span>
</code></pre></div></div>

<p>Load both images inside an inline SVG. The PNG is included as a mask and applied to the JPEG to carve out the transparent area. The result: full transparency with JPEG compression.</p>

<h3 id="caveats">Caveats</h3>
<ol>
  <li><a href="http://codepen.io/shshaw/full/IDbqC/">To work in most browsers</a> the SVG must be inline. You can’t move it into an external file and load it with an <code class="language-plaintext highlighter-rouge">&lt;img&gt;</code> tag.</li>
  <li>No IE8 support</li>
  <li>Masks don’t work in older versions of Android</li>
</ol>]]></content><author><name>Peter Hrynkow</name></author><category term="performance" /><summary type="html"><![CDATA[Wouldn’t it be great if you could get the compression of a JPEG with the transparency of a PNG? You can, with a little layering trick I used while working on the new Sapporo Beer website.]]></summary></entry></feed>