<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Seven Du on Medium]]></title>
        <description><![CDATA[Stories by Seven Du on Medium]]></description>
        <link>https://medium.com/@shiwei?source=rss-64442dec557e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*8Cqy4-fmaTPDwVBUQHA8AA.jpeg</url>
            <title>Stories by Seven Du on Medium</title>
            <link>https://medium.com/@shiwei?source=rss-64442dec557e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 18 Jul 2026 13:28:34 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@shiwei/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How We Cut Database Costs From Thousands of Dollars to Around $400 a Month]]></title>
            <link>https://shiwei.medium.com/how-we-cut-database-costs-from-thousands-of-dollars-to-around-400-a-month-09fee4a652b5?source=rss-64442dec557e------2</link>
            <guid isPermaLink="false">https://medium.com/p/09fee4a652b5</guid>
            <category><![CDATA[cloudflare]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[cost-optimization]]></category>
            <category><![CDATA[prisma]]></category>
            <category><![CDATA[panews]]></category>
            <dc:creator><![CDATA[Seven Du]]></dc:creator>
            <pubDate>Thu, 09 Jul 2026 16:29:15 GMT</pubDate>
            <atom:updated>2026-07-09T16:29:15.182Z</atom:updated>
            <content:encoded><![CDATA[<p>A PANews production migration from self-managed MySQL on Kubernetes to Prisma Postgres and Cloudflare Hyperdrive, with real traffic data.</p><p>PANews is a crypto and Web3 news platform.</p><p>Our previous backend was written in Go, deployed on Kubernetes, and backed by MySQL.</p><p>There was nothing wrong with that stack in isolation. Go is reliable. Kubernetes is powerful. MySQL is mature. The problem was not that the technologies were bad. The problem was the cost structure they created for our business.</p><p>PANews is a content-heavy product. Most traffic is read-heavy, bursty, and repetitive. For that kind of workload, we were carrying too much infrastructure responsibility ourselves: application clusters, database instances, connection pools, scaling, backups, failover, monitoring, and incident response.</p><p>The database bill was thousands of dollars per month, but the operational experience did not feel proportional to the cost. In the early days, when we used an IDC-hosted database, the most painful part was not a single outage. It was that every instability became our problem to diagnose. When the database crashed or connections became unstable, we had to figure out whether the issue was the machine, the network, the database process, the connection pool, or a query pattern in the application.</p><p>Eventually, we made a deliberate architectural simplification:</p><ul><li>We moved from self-managed MySQL to Prisma Postgres.</li><li>We moved the backend runtime to Cloudflare Workers.</li><li>We put Cloudflare Hyperdrive between Workers and Postgres.</li><li>We used Hyperdrive cache for public read paths, while keeping a no-cache path for strong consistency.</li></ul><p>The result was direct: our database cost dropped from thousands of dollars per month to roughly $400 per month.</p><p>This was not just “switching to a cheaper database.” The important change was that the database stopped carrying work it should never have been responsible for in the first place.</p><h3>Expensive Databases Are Often a Symptom</h3><p>Before the migration, the database was handling three different kinds of pressure.</p><p>The first kind was legitimate read and write traffic: publishing articles, updating configuration, changing user state, running admin operations. That is real database work.</p><p>The second kind was repeated public reads: home feeds, article lists, article pages, rankings, columns, recommendations. These requests are frequent, but the underlying data changes much less often than the traffic hits them.</p><p>The third kind was connection pressure from the runtime model. In a Kubernetes setup, we maintained service-side connection pools and provisioned capacity around peak behavior. In a serverless and edge runtime like Cloudflare Workers, direct database access can amplify connection setup, TLS, authentication, and network round trips if there is no layer in between.</p><p>The second and third kinds of pressure were not caused by business complexity. They were architecture cost.</p><p>For a content platform, most public reads do not need millisecond-level strong consistency. A home feed, an article list, or a related-articles block can usually tolerate a few seconds of eventual consistency. If every one of those requests goes directly to the database, the database is effectively being used as a cache.</p><p>That is an expensive cache.</p><p>When traffic grows under that model, you buy a bigger database, more connections, more headroom, and more operational complexity. But a lot of that money is not being spent on true business writes or genuinely fresh reads. It is being spent on repeated reads and connection bursts.</p><h3>Managed Databases Remove Operational Burden, Not Read Amplification</h3><p>Prisma Postgres solved the database maintenance problem for us.</p><p>What we wanted was not an impressive database architecture. We wanted standard Postgres, predictable pricing, reliable connectivity, backups, and fast support when something went wrong.</p><p>In the early Prisma Postgres adoption period, we did run into a few database crashes and stability issues. The difference was that these incidents did not become a long-term self-maintenance burden. Nurul from the Prisma team was extremely responsive, helped us investigate the issues, and pushed improvements forward.</p><p>That experience clarified what “managed” really means.</p><p>Managed does not mean infrastructure will never fail. It means that when infrastructure fails, the product team does not have to debug all the way down from IDC machines, network paths, database processes, and connection pool behavior. The platform team owns the lower layers, and the application team can focus on access patterns, cache boundaries, and business consistency.</p><p>But a managed database alone does not solve the entire cost problem.</p><p>If every public read still goes back to the database, then you have only moved from “self-managed database serving repeated reads” to “managed database serving repeated reads.” Operations get easier, and cost may improve, but the pressure model has not fundamentally changed.</p><p>The real shift came from Hyperdrive cache.</p><h3>Hyperdrive Changed the Database Pressure Model</h3><p>Hyperdrive sits between Cloudflare Workers and the database. For us, it provides two important capabilities: connection pooling and query caching.</p><p>Connection pooling means every Worker request does not have to become direct database connection pressure. Without a layer like Hyperdrive, each Worker invocation that talks directly to the database may pay for TCP, TLS, authentication, and database-level setup. Hyperdrive changes that path: Workers connect to Hyperdrive, while Hyperdrive maintains pooled connections closer to the database.</p><p>Query caching means repeated public reads do not always have to become database queries. Hyperdrive can cache eligible read queries, while writes and uncacheable queries still go to the origin database.</p><p>In production, we use two database paths:</p><ul><li>PG_CACHE: the default path for public reads, with Hyperdrive cache enabled.</li><li>PG_NO_CACHE: the strong-consistency path for admin, write-after-read checks, debugging, and data that must be fresh.</li></ul><p>This split matters more than “turning on cache.”</p><p>Caching everything would be dangerous. Admin screens, permissions, sessions, billing, configuration, and write-after-read validation must not accidentally read stale data. But forcing all public traffic through the strongest consistency path is also wasteful.</p><p>The real design is a consistency boundary:</p><ul><li>Public reads default to the cached path.</li><li>Strong reads must be explicit.</li><li>Writes and write-after-read checks do not depend on cache.</li><li>Critical semantics are not sacrificed to save money.</li><li>A minority of strong-consistency cases should not force the whole system onto the most expensive path.</li></ul><p>Conceptually, it is as simple as this:</p><pre>function databaseBinding(env: Env, strongRead: boolean) {<br>  return strongRead ? env.PG_NO_CACHE : env.PG_CACHE;<br>}</pre><p>The code is not the point. The direction is the point: default to the lower-cost path, then explicitly upgrade consistency only when the business semantics require it.</p><h3>Production Data: Nearly One Billion Hyperdrive Queries in 31 Days</h3><p>Our production PG_CACHE Hyperdrive configuration points to Prisma Postgres. Cache is enabled, stale_while_revalidate is set to 30 seconds, and origin_connection_limit is 60. We did not set an explicit max_age, so it follows Cloudflare&#39;s documented default of 60 seconds.</p><p>As of 2026–07–10 00:06:30 Asia/Shanghai, the production Hyperdrive metrics looked like this:</p><p>WindowHyperdrive querieshit + clienthitHit-class ratioAvg connection latencyAvg query latencyResult bytes24h32,141,84922,235,68769.2%59ms67ms557GB7d218,892,655151,211,99669.1%12ms20ms3.57TB31d997,424,191684,811,98568.7%5ms12ms14.84TB</p><p>hit and clienthit are hit-class statuses we saw in Cloudflare GraphQL metrics. Cloudflare&#39;s public documentation currently explains statuses such as hit, miss, and uncacheable, but does not fully document clienthit, so I am not assigning extra semantics to it here. I treat hit + clienthit simply as the hit-class count reported by Cloudflare metrics.</p><p>The important takeaway is simple: over the last 31 days, Hyperdrive processed almost one billion database-related requests, and roughly 685 million of them were in hit-class states. If all of that traffic had gone directly to the database, the required database CPU, I/O, connection capacity, and tail headroom would have looked very different.</p><p>The connection pool metrics are also useful:</p><p>WindowAvg open connectionsObserved peak open connectionsAvg waiting clientsPeak waiting clients24h2.78121.39619577d2.25120.235295731d2.69120.12511061</p><p>The 31-day average open connection count was only 2.69, while peak waiting clients reached 1061. That tells us two things at the same time: the average state is healthy, but tail pressure still exists.</p><p>This is why platform features are not enough on their own. Application-level details still matter.</p><h3>TTL Should Follow Business Semantics</h3><p>Cache TTL should not be a global guess.</p><p>Longer TTL reduces database pressure, but increases the chance of stale data. Shorter TTL preserves freshness, but gives up some cache benefit. The right answer depends on the business semantics of each endpoint.</p><p>A simple split can look like this:</p><pre>const CACHE_TTL_SECONDS = {<br>  homeFeed: 5,<br>  articleList: 10,<br>  articleDetail: 30,<br>  relatedArticles: 60,<br>  rankings: 300,<br>} as const;</pre><p>The home feed and article lists need shorter windows because users care more about fresh content there. Related articles, rankings, and recommendation blocks can usually tolerate longer windows. Admin, permissions, sessions, and write-after-read flows should not use the cached path.</p><p>Hyperdrive provides the database-level caching layer, but the application still has to produce stable query shapes and choose the right access path.</p><p>Turning on cache is not the same thing as getting a high cache hit rate.</p><h3>Stable Query Shape Is Cache Strategy</h3><p>Cache hit rate depends heavily on query shape.</p><p>If a query uses NOW(), RANDOM(), dynamic SQL fragments, or millisecond-level timestamps, it becomes much harder to reuse cached results. Cloudflare&#39;s documentation also notes that PostgreSQL functions such as NOW() can affect cacheability.</p><p>This is not ideal for a public feed:</p><pre>select id, title, published_at<br>from articles<br>where status = &#39;published&#39;<br>  and published_at &lt;= now()<br>order by published_at desc<br>limit 20;</pre><p>A more cache-friendly approach is to bucket time in the application layer:</p><pre>function timeBucket(seconds: number) {<br>  return new Date(Math.floor(Date.now() / (seconds * 1000)) * seconds * 1000);<br>}</pre><p>Then pass the bucketed timestamp as a query parameter:</p><pre>select id, title, published_at<br>from articles<br>where status = &#39;published&#39;<br>  and published_at &lt;= $1<br>order by published_at desc<br>limit 20;</pre><p>A 5-second, 10-second, or 30-second time bucket sounds small. On a high-frequency public read path, it directly affects cache hit rate, origin query volume, and tail latency.</p><h3>Reduce CPU Time on Public Reads</h3><p>After database cost goes down, another cost often becomes more visible: compute time at the edge.</p><p>Public endpoints should do as little unnecessary work as possible:</p><ul><li>Select only the fields you need.</li><li>Avoid deep-copying large objects.</li><li>Avoid formatting timestamps repeatedly inside loops.</li><li>Do not fetch large result sets just to sort, filter, and trim them in application code.</li><li>Make SQL return a shape close to the API response.</li><li>Keep JSON payloads small.</li></ul><p>For example, a public article list should not fetch full article records, authors, tags, and metrics, then reshape everything in application code. The query should already be close to the response:</p><pre>select<br>  id,<br>  title,<br>  cover_url as &quot;coverUrl&quot;,<br>  published_at as &quot;publishedAt&quot;<br>from articles<br>where status = &#39;published&#39;<br>  and published_at &lt;= $1<br>order by published_at desc<br>limit 20;</pre><p>The thinner the application layer, the lower the CPU time and the more stable the tail latency. Edge runtime is not a reason to do more work per request. It is a reason to finish each request quickly.</p><h3>Reduce Round Trips</h3><p>Tail latency often comes from sequential database round trips, not from one obviously slow SQL query.</p><p>This pattern is expensive:</p><pre>const article = await getArticle(id);<br>const author = await getAuthor(article.authorId);<br>const tags = await getTags(id);</pre><p>If the data is tightly related, fetch it in one query or at least parallelize independent queries.</p><p>For an article page, author and tags can often be fetched together:</p><pre>select<br>  a.id,<br>  a.title,<br>  u.name as &quot;authorName&quot;,<br>  coalesce(json_agg(t.name) filter (where t.id is not null), &#39;[]&#39;) as tags<br>from articles a<br>left join users u on u.id = a.author_id<br>left join article_tags at on at.article_id = a.id<br>left join tags t on t.id = at.tag_id<br>where a.id = $1<br>group by a.id, u.name;</pre><p>This is not about writing clever SQL for its own sake. It is about reducing network round trips. In a Workers-to-regional-database path, each extra round trip directly increases tail latency.</p><h3>Scope Database Clients to Requests</h3><p>When using Hyperdrive from Workers, do not create a database client in global scope, and do not add another global driver-level connection pool.</p><p>Cloudflare recommends creating the client inside the request handler. Hyperdrive maintains the underlying pool to the database. Worker-to-Hyperdrive client connections are cleaned up with the invocation lifecycle.</p><p>Conceptually:</p><pre>import { Client } from &#39;pg&#39;;</pre><pre>export default {<br>  async fetch(_request: Request, env: Env) {<br>    const client = new Client({<br>      connectionString: env.PG_CACHE.connectionString,<br>    });</pre><pre>    await client.connect();</pre><pre>    const result = await client.query(<br>      `<br>      select id, title, published_at<br>      from articles<br>      where status = &#39;published&#39;<br>      order by published_at desc<br>      limit 20<br>      `,<br>    );</pre><pre>    return Response.json(result.rows);<br>  },<br>};</pre><p>The important part is not manually holding a database connection forever. In Workers with Hyperdrive, Hyperdrive owns the lower-level database pool. Application code should avoid global clients, extra connection pools, long transactions, and slow request lifecycles.</p><p>For local scripts and one-off operational tasks, the rule is different. If a script connects directly to the database, close the connection explicitly:</p><pre>const client = new Client({ connectionString });<br>await client.connect();</pre><pre>try {<br>  await client.query(&#39;select 1&#39;);<br>} finally {<br>  await client.end();<br>}</pre><p>Worker request handling and local operational scripts are different environments. Hyperdrive should pool database connections for Workers. Scripts should clean up after themselves.</p><h3>Keep Transactions Short</h3><p>The connection pool metrics showed healthy averages, but they also showed tail pressure. One common cause is long connection occupancy.</p><p>Long transactions, slow queries, external API calls inside transactions, and heavy application computation while holding a connection all increase waiting clients.</p><p>Keep transactions short. Put only the database operations that must be atomic inside the transaction. External requests, AI calls, object storage, search indexing, push notifications, and other side effects should happen outside the transaction or move to asynchronous jobs.</p><p>This is a basic rule, but it has a real effect on tail latency and connection pool health.</p><h3>What Actually Changed</h3><p>The core lesson from this migration is not “Postgres is cheaper than MySQL” or “Workers are better than Kubernetes.”</p><p>The real change was this:</p><blockquote><em>From self-managed infrastructure and a database absorbing all read traffic</em></blockquote><p>&gt;</p><blockquote><em>To managed Postgres, edge connection pooling, controlled caching, and explicit strong-consistency paths</em></blockquote><p>The database stores facts.</p><p>Hyperdrive handles connection reuse and public read caching.</p><p>Workers serve requests close to users.</p><p>The application decides which reads need strong consistency and which reads can tolerate short eventual consistency.</p><p>The result was clear:</p><ul><li>Database cost dropped from thousands of dollars per month to roughly $400.</li><li>IDC database instability and manual maintenance stopped being a daily concern.</li><li>Public read traffic no longer directly determined database size.</li><li>Over the last 31 days, about 68.7% of Hyperdrive query states were hit-class states.</li><li>The team stopped spending energy on Kubernetes, database instances, and connection incidents.</li></ul><p>My current view on database cost reduction is simple:</p><p><strong>Do not make the database act as your cache. Do not make a small product team act as a database operations team. Do not force every request onto the most expensive consistency path just because a minority of requests truly need it.</strong></p><p>A database should be stable, affordable, and quiet. It should store facts. It should not become the most expensive, most fragile, and most attention-hungry part of the system.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=09fee4a652b5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Prisma Next in Practice: A Contract-First Data Layer for Humans and Agents]]></title>
            <link>https://shiwei.medium.com/prisma-next-in-practice-a-contract-first-data-layer-for-humans-and-agents-8c635803b05d?source=rss-64442dec557e------2</link>
            <guid isPermaLink="false">https://medium.com/p/8c635803b05d</guid>
            <category><![CDATA[typescript]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[prisma]]></category>
            <dc:creator><![CDATA[Seven Du]]></dc:creator>
            <pubDate>Wed, 08 Jul 2026 13:33:57 GMT</pubDate>
            <atom:updated>2026-07-08T13:33:57.610Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AGGXwHDPyQpmnBWzUDvHeQ.jpeg" /></figure><p>I have been using Prisma Next in a real agent product, not as a toy example and not as a benchmark. The product has users, teams, billing, approvals, scheduled tasks, webhooks, a public discovery surface, and an assistant that can help operate parts of the system. The data model has not stayed still.</p><p>That is where Prisma Next became interesting to me.</p><p>The main value I felt was not “a nicer ORM.” It was the contract-first shape: the data contract is the source of truth, and the rest of the system follows from it. Types, runtime access, migrations, and database verification all point back to one explicit place.</p><p>That sounds small until the product starts moving quickly.</p><h3>The Problem Is Not Just Queries</h3><p>Most database tooling discussions start with query ergonomics. Can I write a join cleanly? Are filters type-safe? Is the generated client pleasant to use?</p><p>Those things matter, but they are not the whole data-layer problem in a product that keeps changing.</p><p>The harder problems are usually less glamorous:</p><ul><li>Which data model is the current product model?</li><li>Did the migration that production needs actually land?</li><li>Are the generated types still describing the database we are connected to?</li><li>Can another engineer, or an agent, continue this work without reverse-engineering every hidden assumption?</li></ul><p>In a small app, these questions are manageable by memory. In a product with accounts, teams, access control, approvals, billing events, integrations, publication metadata, and assistant state, memory stops being enough.</p><p>The system needs a contract.</p><h3>What Prisma Next Got Right</h3><p>The strongest idea in Prisma Next is that the contract is not just a schema file sitting next to the real work. It is the work.</p><p>In my project, the contract lives as a TypeScript data contract. The runtime reads the emitted contract artifact and exposes a typed Postgres runtime for serverless execution. Migrations live alongside the contract, so the history of the data model is reviewable instead of implied.</p><p>The day-to-day loop is direct:</p><pre>edit contract -&gt; emit -&gt; plan migration -&gt; review -&gt; migrate -&gt; verify</pre><p>That loop is not exciting, and that is the point. It is explicit. It gives both humans and agents a narrow path to follow.</p><p>When the product model changes, I do not want the data layer to be clever. I want it to be honest. I want to see the contract change, the migration plan, and the verification result.</p><h3>A Real Product Shape Changes Under You</h3><p>One lesson from building an agent product is that the nouns change.</p><p>Early systems often start with low-level implementation nouns: runs, triggers, steps, events. Those are useful, but they are not always the durable product model. As the product matures, the useful user-facing nouns become higher level: agents, inboxes, tasks, schedules, automations, activity.</p><p>That kind of shift is where data layers get messy. Old names stay in tables. New names appear in APIs. Some runtime concepts become product concepts by accident. A few JSON fields become load-bearing because nobody wants to stop and model them properly.</p><p>Prisma Next does not magically solve product modeling. No tool does. But contract-first development forces the conversation into the right place:</p><p>What is the product relationship now?</p><p>What deserves a model?</p><p>What should remain runtime state?</p><p>What needs a constraint, an index, or a migration?</p><p>That pressure is useful. It turns product drift into a concrete contract diff instead of a vague feeling that the codebase is getting harder to reason about.</p><h3>Migrations Feel Better When They Are Reviewable</h3><p>I care a lot about production changes being boring. For database work, boring means observable and recoverable.</p><p>The flow I ended up trusting was simple:</p><ol><li>inspect the contract change</li><li>review the planned migration</li><li>apply it deliberately</li><li>verify the live database against the expected contract</li></ol><p>The important part is not that there is a command for each step. The important part is that each step produces evidence.</p><p>If the generated types are stale, re-emit the contract. If the database does not match, verify catches drift. If a migration fails, the contract and migration state tell you where you are instead of leaving you to infer it from application errors.</p><p>That changes the emotional texture of database work. It is less “I hope this push did the right thing” and more “I can see what changed, what ran, and what the database currently claims to be.”</p><h3>Why It Works Well With Agents</h3><p>The most surprising part of the experience was how well this model fits agent-assisted development.</p><p>Agents are not weak because they cannot type code. They are weak when the environment is full of implicit state. If the only way to understand the data layer is to read a dozen files, remember a Slack thread, inspect production by hand, and guess which migration has run, the agent will eventually make something up.</p><p>Prisma Next reduces that guessing.</p><p>An agent can read the contract. It can inspect migrations. It can run emit, plan, migrate, and verify. It can connect the runtime types back to the contract artifact. The workflow has observable checkpoints.</p><p>That matters more than it first appears.</p><p>For a human, a contract-first flow is a discipline. For an agent, it is a rail. It narrows the possible next actions and makes failures easier to classify.</p><p>When something breaks, the question becomes concrete:</p><ul><li>Is the contract source wrong?</li><li>Were the artifacts emitted?</li><li>Was the migration planned?</li><li>Did the migration apply?</li><li>Does the database verify against the contract?</li></ul><p>Those are much better questions than “why is the ORM acting weird?”</p><p>The smoothness comes from less guessing, not fewer commands.</p><h3>Client-Owned Memory, Server-Owned Data</h3><p>Another design lesson from the product was to keep memory and durable data separate.</p><p>The assistant has client-owned memory and session state. That belongs close to the user experience: preferences, facts, task context, and compressed conversation checkpoints. It should not silently become part of the database contract unless it is truly product data.</p><p>The server-side product model is different. Teams, permissions, billing, approvals, published templates, and operational state need durable structure. They need constraints. They need migration history. They need to be verified.</p><p>That separation made Prisma Next feel like the right tool for the durable side. It gives the product data a contract without pretending every piece of assistant context belongs in the same layer.</p><p>This is an important distinction for agent products. If everything becomes “memory,” the product loses structure. If everything becomes relational data, the assistant becomes heavy and unnatural. The contract helps draw the line.</p><h3>Where There Is Still Friction</h3><p>The experience was not frictionless.</p><p>Some parts still feel early. Project layout and scaffolding details matter more than they should. Production database connections can still fail in ways that require careful recovery. Migration tooling is only as good as the operational discipline around it.</p><p>But those are surface-area issues. They are real, but they do not undermine the core model.</p><p>The core model is strong: make the data contract explicit, derive the runtime from it, make migrations reviewable, and verify the database instead of assuming it is correct.</p><p>That is the part I would keep.</p><h3>The Real Takeaway</h3><p>Prisma Next is not automatically the lowest-cost choice for every project.</p><p>If you have three tables, a few raw SQL queries, and no meaningful schema evolution, a lighter setup may be better. I would not add contract machinery just to feel sophisticated.</p><p>But if your product model is evolving, and especially if agents are going to help maintain or operate the system, the tradeoff changes.</p><p>In that world, the best data layer is not just the one with the prettiest query API. It is the one that makes state explicit, migrations reviewable, and correctness verifiable.</p><p>That is what Prisma Next made clear to me.</p><p>Contract-first is not only a nicer way for humans to model data. It is also a better working surface for agents.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8c635803b05d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Patchwork Fixed a Real Dart Dependency Bug Without a Fork]]></title>
            <link>https://shiwei.medium.com/how-patchwork-fixed-a-real-dart-dependency-bug-without-a-fork-b5033475d2fd?source=rss-64442dec557e------2</link>
            <guid isPermaLink="false">https://medium.com/p/b5033475d2fd</guid>
            <category><![CDATA[developer-tools]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[dart]]></category>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Seven Du]]></dc:creator>
            <pubDate>Wed, 08 Jul 2026 12:59:23 GMT</pubDate>
            <atom:updated>2026-07-08T12:59:23.984Z</atom:updated>
            <content:encoded><![CDATA[<h3>Oxy carried a narrow Retry-After fix as a reviewable provider overlay instead of forking http_parser.</h3><figure><img alt="Abstract HTTP retry dependency patch workflow without a fork" src="https://cdn-images-1.medium.com/proxy/1*w9CEV5D0fsupDjU9bei1nw.jpeg" /></figure><p>The first two Patchwork articles were about the model: stop editing .pub-cache, commit dependency patches as reviewable artifacts, and keep ownership boundaries clear.</p><p>This one is about proof.</p><p>Oxy, a policy-first HTTP client for Dart and Flutter, already uses Patchwork to carry a real dependency fix. The bug was not in Oxy’s own source. It lived in http_parser, a direct runtime dependency. But the behavior was visible through Oxy’s retry policy, so Oxy still owned the user-facing result.</p><p>That is exactly the kind of edge case Patchwork was built for.</p><h3>The bug: Retry-After depends on HTTP-date parsing</h3><p>HTTP retry behavior is not just exponential backoff. Servers can send Retry-After to say when a client should try again.</p><p>That header can be an integer number of seconds, or it can be an HTTP-date. Oxy’s RetryPolicy respects that header. If a response includes a valid Retry-After date, Oxy uses it instead of its normal backoff delay.</p><p>The parsing path is intentionally small: Oxy reads the header, tries seconds first, and then delegates HTTP-date parsing to package:http_parser.</p><p>That is the right dependency choice. Oxy should not carry its own full HTTP-date parser just to implement retry behavior.</p><p>But it also means a parser edge case can become an Oxy behavior bug.</p><h3>The edge case: obsolete RFC850 dates</h3><p>HTTP-date has historical formats. One of them is the obsolete RFC850 form, such as a weekday name, a two-digit year, and GMT.</p><p>The tricky part is the two-digit year.</p><p>The old implementation in http_parser treated that form as 1900 plus the two-digit value. That makes some modern dates land in the wrong century. For Retry-After, that can change whether a retry delay is in the future, in the past, or effectively zero.</p><p>The narrow fix is not dramatic. The parser should interpret the two-digit year relative to the current century, then roll it back by 100 years if the result is more than 50 years in the future. That is the RFC 9110 rule the Oxy patch follows.</p><p>Small fix. Real behavior. Wrong dependency layer.</p><h3>The usual options are awkward</h3><p>When a bug lives in a dependency, a package maintainer has a few familiar choices.</p><p>They can fork the dependency. That works, but now a small date-parsing fix becomes a long-lived package ownership problem. Every future dependency update has to account for the fork.</p><p>They can wait for upstream. That is the best long-term outcome, but it may not unblock the package release that needs correct behavior now.</p><p>They can use dependency_overrides locally. That can make tests pass on one machine, but it does not explain why the replacement exists, does not make the patch reviewable as a package artifact, and does not compose cleanly for downstream users.</p><p>Oxy needed a smaller boundary.</p><h3>What Oxy committed</h3><p>Oxy uses Patchwork’s package-provided patch workflow.</p><p>The important files are ordinary repository files:</p><ul><li>pubspec.yaml depends on http_parser and patchwork as runtime dependencies.</li><li>patchwork.yaml declares that Oxy provides a patch for http_parser 4.1.2.</li><li>patches/http_parser@4.1.2.patch contains the actual source diff.</li><li>CHANGELOG.md records the fix under Oxy 0.6.0.</li><li>policy tests verify the visible Retry-After behavior.</li></ul><p>That shape matters.</p><p>The patch is not hidden in a cache. It is not buried in a fork. It is not an unexplained override. It is a reviewed file next to the package that needs the behavior.</p><p>The provider manifest is also explicit. It names http_parser, pins the source identity, points at the patch file, and records the reason: fixing RFC850 two-digit HTTP-date years used by Retry-After.</p><p>That is what makes the dependency patch auditable.</p><h3>Why this is a provider overlay, not a root patch</h3><p>This is not just an app carrying a local workaround.</p><p>Oxy is a published package. Its users depend on Oxy for HTTP client behavior. They should not have to copy an http_parser patch into every downstream application just to get correct retry behavior from Oxy.</p><p>At the same time, Oxy should not be allowed to silently patch anything in a downstream dependency graph. That would be too broad.</p><p>The boundary is direct runtime dependency ownership.</p><p>Oxy directly depends on http_parser. Oxy observes the Retry-After behavior. Oxy carries the regression tests. Oxy declares the provider overlay. Downstream applications receive the composed behavior without owning the workaround.</p><p>That is the difference between convenience and ownership.</p><h3>The proof is in the tests</h3><p>The strongest part of this case is not the patch file. It is the regression surface around it.</p><p>Oxy has tests for normal HTTP-date Retry-After values. It has tests for RFC850 dates in the current century. It has tests for the rollover case where the interpreted date would otherwise be more than 50 years in the future. It also has tests for invalid Retry-After values and obsolete dates that should resolve to zero delay.</p><p>Those tests make the patch measurable as Oxy behavior, not just dependency surgery.</p><p>That is an important promotion point for Patchwork: a dependency patch should not end at “the diff applies.” The package carrying the patch should prove the behavior it needs.</p><p>Patchwork supplies the artifact boundary. The package still owns the product behavior.</p><h3>What this avoids</h3><p>This Oxy fix avoided a fork of http_parser for one small parser edge case.</p><p>It avoided treating pubspec_overrides.yaml as durable truth.</p><p>It avoided asking every downstream application to understand an implementation detail of Oxy’s retry policy.</p><p>It avoided pretending the issue did not exist until upstream released a fix.</p><p>Instead, the repository now has a compact story:</p><ul><li>here is the dependency version;</li><li>here is the reviewed patch;</li><li>here is why the package provides it;</li><li>here are the tests that prove the visible behavior;</li><li>here is the changelog entry that tells users what changed.</li></ul><p>That is the shape dependency patching should have.</p><h3>The upstream path still matters</h3><p>Patchwork does not make upstream irrelevant.</p><p>The best long-term outcome for this kind of fix is still upstream adoption. When the dependency releases a corrected version, Oxy can remove the provider overlay and the patch file in a normal dependency update.</p><p>That removal path is part of the point.</p><p>A good temporary patch should be easy to find, easy to review, and easy to delete when it is no longer needed.</p><p>That is harder when the fix lives in a fork. It is much harder when the fix lives in someone’s local pub cache.</p><h3>The lesson</h3><p>The value of Patchwork is not that it can modify dependency code. You can always modify dependency code if you are willing to make enough mess.</p><p>The value is that it gives dependency fixes a disciplined place to live.</p><p>In Oxy’s case, Patchwork let a Dart package ship correct Retry-After behavior without taking ownership of an http_parser fork. The fix stayed narrow. The reason stayed visible. The tests stayed in the package that owned the behavior. Downstream users did not have to manage the workaround themselves.</p><p>That is the real pitch:</p><p>Patchwork turns a dependency workaround into a reviewable package artifact.</p><p>You can find Patchwork on pub.dev at <a href="https://pub.dev/packages/patchwork">https://pub.dev/packages/patchwork</a> and on GitHub at <a href="https://github.com/medz/patchwork">https://github.com/medz/patchwork</a>.</p><p>The Oxy repository is at <a href="https://github.com/medz/oxy">https://github.com/medz/oxy</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b5033475d2fd" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[When a Dart Package Needs to Patch Its Own Dependency]]></title>
            <link>https://shiwei.medium.com/when-a-dart-package-needs-to-patch-its-own-dependency-ecd445e52fdb?source=rss-64442dec557e------2</link>
            <guid isPermaLink="false">https://medium.com/p/ecd445e52fdb</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[dart]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[developer-tools]]></category>
            <category><![CDATA[open-source]]></category>
            <dc:creator><![CDATA[Seven Du]]></dc:creator>
            <pubDate>Mon, 06 Jul 2026 01:49:11 GMT</pubDate>
            <atom:updated>2026-07-06T01:49:11.812Z</atom:updated>
            <content:encoded><![CDATA[<h3>Package-provided patches are about ownership, not convenience.</h3><figure><img alt="Abstract ownership boundary for a package-provided dependency patch" src="https://cdn-images-1.medium.com/proxy/1*cx_orx5Nk_1CHmamjbEDAA.jpeg" /></figure><p>The first Patchwork article was about the obvious pain: editing .pub-cache is fast, but it is not a workflow. A dependency patch should be reviewable, committed, and reproducible.</p><p>That story mostly fits apps and workspaces. The root project needs a patched dependency while it runs, so the root project owns the patch file.</p><p>The second workflow is more subtle.</p><p>What happens when a published Dart package needs to carry a small fix for one of its own runtime dependencies? The downstream application should not have to copy your patch file. It should not need to understand your temporary workaround. It should depend on your package and run normally.</p><p>That is the problem package-provided patches are designed for.</p><h3>The easy case: your app owns the patch</h3><p>Root-project patches are easy to explain because the owner is obvious.</p><p>If an app needs patched behavior, the app carries the patch. It creates an editable copy of the resolved dependency, commits the edit as patches/&lt;pkg&gt;@&lt;version&gt;.patch, and regenerates local override wiring when needed.</p><p>That patch can target a direct dependency or a transitive dependency selected by the current pub resolution. The key point is not whether the package is listed directly in pubspec.yaml. The key point is that the root project is the runtime owner. It is the thing that needs the patched behavior.</p><p>For a root-owned patch, the durable file is simple:</p><p>patches/&lt;pkg&gt;@&lt;version&gt;.patch</p><p>The rest is activation state. .patchwork/ is an edit workspace. .dart_tool/patchwork/ is generated output. pubspec_overrides.yaml wires the patched copy into local resolution.</p><p>That is a good boundary for apps.</p><p>But package authors have a different problem.</p><h3>The harder case: a package ships behavior downstream</h3><p>Imagine you maintain a Dart package that depends on another package at runtime. There is a narrow bug in that dependency. You can reproduce it through your package. You can patch it locally. You may even have an upstream pull request open.</p><p>But your users do not depend on that lower-level package because they care about it. They depend on your package.</p><p>If every downstream app has to copy your patch file, your package has leaked implementation detail into every consumer project. If every downstream app has to add its own Patchwork dependency and register your workaround manually, the ownership boundary is wrong.</p><p>The package that needs the runtime fix should own the reviewed patch.</p><p>That is the purpose of package-provided patches: a published package can carry a reviewed patch for one of its direct runtime dependencies, and downstream applications can receive the composed behavior without copying the patch into their own repository.</p><h3>Why direct runtime dependencies matter</h3><p>Package-provided patches are intentionally narrower than root-project patches.</p><p>A root project can patch direct or transitive packages selected by its current pub resolution because the root project owns the whole runtime composition. It is allowed to say: this app needs this resolved package to behave differently.</p><p>A published package does not own the entire downstream application.</p><p>That is why package-provided patches should be limited to direct runtime dependencies of the providing package. Not dev dependencies. Not unrelated packages. Not arbitrary transitive packages several layers away.</p><p>This is not a technical inconvenience. It is the product boundary.</p><p>If a package could silently contribute patches for anything in the downstream graph, it would become hard to reason about who is responsible for a dependency change. Direct runtime dependency is the line where responsibility is still legible: the providing package depends on it, observes the behavior, reviews the patch, and declares why it is carrying the change.</p><h3>Patch files need review before registration</h3><p>A root-project patch is already reviewable, but a package-provided patch needs a stricter review habit.</p><p>The patch is no longer only a local application workaround. It can affect downstream applications that depend on your package. That means the patch file should be narrow, runtime-oriented, and intentional.</p><p>A package-provided patch should include the source files required for the runtime fix. It should not casually ship upstream tests, examples, documentation edits, formatting churn, or unrelated maintenance. Those changes may be useful for an upstream pull request, but they are not usually part of what downstream applications need to run.</p><p>That is why Patchwork treats overlay registration as a separate step. You do not create a package-provided patch merely by editing a dependency. You create the patch, review the file, and then register it.</p><p>A minimal shape looks like this:</p><p>dart pub add patchwork<br>dart pub get<br>dart run patchwork patch collection<br># edit .patchwork/collection@&lt;version&gt;/<br>dart run patchwork commit collection<br># review patches/collection@&lt;version&gt;.patch<br>dart run patchwork overlay add collection --reason &quot;Fix parser behavior used by this package.&quot;</p><p>The files that belong in the published package are:</p><p>patchwork.yaml<br>patches/&lt;pkg&gt;@&lt;version&gt;.patch</p><p>That commit says two things: here is the patch artifact, and here is the provider manifest that declares when and why this package contributes it.</p><h3>patchwork.yaml is not a second patch system</h3><p>The provider manifest is easy to misunderstand.</p><p>patchwork.yaml is not the patch itself. It is not a lockfile. It is not a hidden source of truth competing with patches/&lt;pkg&gt;@&lt;version&gt;.patch.</p><p>The patch file remains the durable change. patchwork.yaml records the reviewed contribution: package name, version, source identity, patch path, and reason. Downstream hook logic can then inspect provider manifests, match them against the resolved dependency graph, and compose the generated output when the target package version and source identity match.</p><p>That design keeps the same principle as the root workflow: committed files describe intent, generated files can be recreated.</p><h3>Downstream apps should stay boring</h3><p>The point of package-provided patches is not to make downstream applications think about more machinery. It is the opposite.</p><p>A downstream app should depend on the providing package and run its normal workflow. It should not copy another package’s patch file. It should not register a workaround it does not own. It should not become responsible for reviewing a dependency change whose behavior is only observable through the package it consumes.</p><p>The provider owns the patch because the provider owns the runtime dependency relationship.</p><p>That ownership also gives the provider a clear removal path. When upstream releases the fix, the package can remove patchwork.yaml and the patch file in one ordinary package update. Consumers receive the cleanup through the same dependency update path they already use.</p><h3>The social contract</h3><p>Dependency patching can look suspicious if it is framed as a way to silently modify the ecosystem.</p><p>That is not the goal.</p><p>A package-provided patch should be a release unblock, a compatibility bridge, or a narrow behavior adjustment that stays visible in the providing package’s source. The patch should be reviewed. The reason should be written down. Upstream should still be the long-term home for the fix whenever possible.</p><p>Patchwork is useful because it makes that temporary state explicit instead of pretending it does not exist.</p><p>The real question is not “can this package patch that dependency?”</p><p>The better question is: who owns the behavior, who should review the change, and who should have to know it exists?</p><p>For root projects, the answer is usually the app or workspace.</p><p>For package-provided patches, the answer is narrower: the package that directly depends on the runtime dependency, carries the reviewed patch, and shields downstream applications from the workaround.</p><p>That boundary is what keeps dependency patching understandable.</p><p>Patchwork is available on pub.dev at <a href="https://pub.dev/packages/patchwork">https://pub.dev/packages/patchwork</a> and on GitHub at <a href="https://github.com/medz/patchwork">https://github.com/medz/patchwork</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ecd445e52fdb" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Stop Editing `.pub-cache`: Reviewable Dependency Patches for Dart]]></title>
            <link>https://shiwei.medium.com/stop-editing-pub-cache-reviewable-dependency-patches-for-dart-6bfb40044afb?source=rss-64442dec557e------2</link>
            <guid isPermaLink="false">https://medium.com/p/6bfb40044afb</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[dart]]></category>
            <category><![CDATA[developer-tools]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Seven Du]]></dc:creator>
            <pubDate>Sun, 05 Jul 2026 15:14:51 GMT</pubDate>
            <atom:updated>2026-07-05T15:14:51.962Z</atom:updated>
            <content:encoded><![CDATA[<h3>Patchwork turns temporary dependency fixes into committed, reproducible project artifacts.</h3><figure><img alt="Abstract workflow showing Dart dependency edits becoming a reviewable patch file" src="https://cdn-images-1.medium.com/proxy/1*HpWWX60lFvxLohqT05ddWA.jpeg" /></figure><p>Every Dart developer has made the same compromise at least once.</p><p>A dependency has a small bug. The fix is obvious. Upstream may accept it, but not before your release. Forking the package feels too heavy. Editing .pub-cache works for five minutes, then disappears the moment someone else runs the project.</p><p>That gap is where Patchwork lives.</p><p>Patchwork is a patch manager for Dart pub dependencies. It does not ask you to fork every package you touch, and it does not treat your global pub cache as a workspace. Instead, it turns a dependency change into a normal project artifact: a versioned patch file that can be committed, reviewed, and reapplied wherever the project runs.</p><h3>Dependency fixes deserve code review</h3><p>Most dependency patching starts as a local survival tactic. You find the package in .pub-cache, make the change, run the app, and confirm the bug is gone. That is useful as a debugging move, but it is a poor engineering boundary.</p><p>A cache edit has no durable identity. It is not in the repository. It is not visible in pull requests. It does not run in CI. It does not tell the next developer why it exists. It may even disappear during a routine pub cache repair or SDK refresh.</p><p>A fork solves some of that, but it introduces a different cost. Now a small compatibility fix becomes a new upstream remote, new release discipline, and often a long-lived divergence from the package you actually wanted to depend on. For some changes, a fork is absolutely the right answer. For many release unblocks and narrow compatibility fixes, it is heavier than the problem.</p><p>The useful middle ground is simple: keep the change close to the project that needs it, but make it reviewable.</p><p>That is the core idea behind Patchwork.</p><h3>The model</h3><p>Patchwork separates four things that are often blurred together:</p><ul><li>pubspec.lock tells you which dependency version the project resolved.</li><li>.pub-cache remains the original package source, not your editing workspace.</li><li>.patchwork/&lt;package&gt;@&lt;version&gt;/ is a disposable edit directory.</li><li>patches/&lt;package&gt;@&lt;version&gt;.patch is the durable artifact you commit.</li></ul><p>The generated local wiring lives in pubspec_overrides.yaml and .dart_tool/patchwork/. Those files are activation state. They can be recreated from the committed patch file and the current pub resolution.</p><p>That split is important. Patchwork does not make pubspec_overrides.yaml the source of truth. The patch file is the source of truth. Overrides are how the patched dependency is activated locally.</p><h3>A root-project patch</h3><p>For an app or workspace that owns its dependency patch, the workflow is intentionally small:</p><pre>dart pub add dev:patchwork<br>dart pub get<br>dart run patchwork doctor<br>dart run patchwork patch collection<br># edit .patchwork/collection@&lt;version&gt;/<br>dart run patchwork commit collection<br>dart run patchwork apply collection</pre><p>After commit, the file that matters is:</p><pre>patches/collection@&lt;version&gt;.patch</pre><p>That file is what belongs in version control. It can be reviewed like any other source change. A teammate or CI job can apply it without knowing which local cache directory you edited when you first found the fix.</p><p>Patchwork also supports root-owned patches for transitive packages selected by the current pub resolution. That matters in real projects because the bug you need to patch is not always one of the packages listed directly in your pubspec.yaml. The owner is still the root project: the app or workspace carries the patch because it is the thing that needs the patched behavior at runtime.</p><h3>Package-provided patches</h3><p>There is a second workflow for published packages that need to carry a patch for one of their direct runtime dependencies.</p><p>That case is deliberately stricter. A package-provided patch is not just a local project convenience; it can affect downstream applications. Before registering it, you should review the patch file and keep it narrow. Include runtime source files required for the fix. Do not publish upstream tests, examples, docs, or unrelated maintenance unless those files are actually needed at runtime.</p><p>In that workflow, the package commits both:</p><pre>patchwork.yaml<br>patches/&lt;pkg&gt;@&lt;version&gt;.patch</pre><p>Downstream applications should not have to copy that patch file or add their own Patchwork dependency just to consume the providing package. Patchwork’s package hook can discover the provider manifest and compose matching patches when the resolved dependency version and source identity match.</p><p>That is a more advanced feature, but the principle is the same: the reviewed patch file is the durable artifact, and generated output remains generated output.</p><h3>What Patchwork is not</h3><p>Patchwork is not a new package resolver. It is not a replacement for upstream contribution. It is not a reason to keep private patches forever.</p><p>The best ending for most dependency fixes is still upstream: open the issue, send the pull request, remove the local patch when a release lands.</p><p>Patchwork is for the part in between: the days or weeks where your project needs to move, the change is small enough to review locally, and the team needs a reproducible way to carry it.</p><p>It also avoids pretending that dependency_overrides are a complete workflow. Overrides are useful, but they are only wiring. They say where pub should point. They do not explain how the replacement source was produced, whether it still matches the resolved dependency version, or what should be committed.</p><p>A good patch workflow should make those boundaries explicit.</p><h3>Why this matters for Dart</h3><p>Dart projects already have a strong package ecosystem and a predictable pub workflow. That is exactly why dependency patching should feel boring.</p><p>When a patch is needed, the workflow should be ordinary:</p><ul><li>create an editable copy of the resolved dependency;</li><li>make the smallest required change;</li><li>commit a versioned patch file;</li><li>apply it consistently in local development and CI;</li><li>remove it when upstream catches up.</li></ul><p>No mystery cache edits. No hidden machine state. No unexplained overrides.</p><p>That is the design goal for Patchwork: reviewable dependency patches for Dart pub packages, with the durable truth living in your repository.</p><p>You can find Patchwork on pub.dev at <a href="https://pub.dev/packages/patchwork">https://pub.dev/packages/patchwork</a> and on GitHub at <a href="https://github.com/medz/patchwork">https://github.com/medz/patchwork</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6bfb40044afb" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[HTTP Requests Are Easy. Reliable Dart API Clients Are Hard.]]></title>
            <link>https://shiwei.medium.com/http-requests-are-easy-reliable-dart-api-clients-are-hard-686849ed2b0f?source=rss-64442dec557e------2</link>
            <guid isPermaLink="false">https://medium.com/p/686849ed2b0f</guid>
            <category><![CDATA[dart]]></category>
            <dc:creator><![CDATA[Seven Du]]></dc:creator>
            <pubDate>Sat, 20 Jun 2026 17:34:13 GMT</pubDate>
            <atom:updated>2026-06-20T17:50:01.947Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dYv_AV3qsM6p2t2BVVCduA.png" /></figure><p><em>From copied request snippets to a policy-first network layer: Oxy is not about sending one request, but maintaining many.</em></p><p><em>The hard part of a real network layer is rarely “how do I send one request?” It is “how do I keep many requests reliable over time?”</em></p><p>The first version of a network layer usually looks harmless.</p><p>Send a GET, parse some JSON, map it into a model. Send a POST, attach a token, handle an error. The code is short, the feedback loop is fast, and adding an abstraction can feel unnecessary.</p><p>The trouble usually starts around the twentieth request.</p><p>That is when a team begins to run into familiar problems that are easy to recognize and hard to keep consistent:</p><ul><li>Some requests have a timeout. Some do not.</li><li>One call site throws on 404. Another returns null.</li><li>Some requests retry. Some do not, and the reason only exists in someone’s memory.</li><li>A file upload fails and gets retried, but the second attempt is using a stream that has already been consumed.</li><li>Flutter Web and native transport differences start leaking into business code.</li><li>API client tests require a local mock server, so many paths quietly go untested.</li><li>Callers only know that “the network failed”, not whether the failure was a timeout, status error, decode error, cancellation, or policy rejection.</li></ul><p>None of these problems is dramatic on its own. The real danger is that every call site starts answering the same set of network questions in a slightly different way.</p><p>Dart can already send HTTP requests. package:http does a good job of solving the problem of sending one request.</p><p><a href="https://pub.dev/packages/oxy">Oxy</a> is focused on the next stage: when a Dart or Flutter project needs to maintain a set of API clients, how do you keep the network layer consistent, testable, and explainable?</p><p>Put differently: <strong>an HTTP call is easy. A reliable API client is hard.</strong></p><h3>Copy-Paste Is Not the Real Problem</h3><p>Many projects start with code like this:</p><pre>Future&lt;Map&lt;String, Object?&gt;&gt; getUser(String id) async {<br>  final response = await http<br>      .get(Uri.parse(&#39;$baseUrl/users/$id&#39;), headers: authHeaders())<br>      .timeout(const Duration(seconds: 10));  if (response.statusCode == 401) {<br>    throw AuthExpired();<br>  }<br><br>  if (response.statusCode &lt; 200 || response.statusCode &gt;= 300) {<br>    throw ApiException(response.statusCode, response.body);<br>  }<br><br>  return jsonDecode(response.body) as Map&lt;String, Object?&gt;;<br>}</pre><p>There is nothing wrong with this function.</p><p>The problem is that it gets copied into getPosts, createOrder, uploadAvatar, background sync jobs, and temporary methods inside Flutter screens. Each copy brings along a hidden network contract:</p><ul><li>How is the URL resolved?</li><li>Who adds auth headers?</li><li>Is timeout a shared policy or a local habit?</li><li>When does a status code become an error?</li><li>What is safe to retry?</li><li>Can the response body be read again?</li><li>Does the error object carry enough context for callers and logs?</li></ul><p>If those answers are scattered across dozens of functions, you no longer have an API client that can be reviewed as a module. You have many similar request snippets with subtly different behavior.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MCCv7azj-BZTuolsk15RCg.png" /></figure><p><em>One HTTP call is simple. Once auth, timeout, status handling, and retry logic are copied across many requests, the network layer becomes difficult to reason about.</em></p><p>Oxy is not trying to save a few lines of code.</p><p>Saving a few lines is not the core value of a network layer. The real value is moving repeated decisions out of call sites and into policies that a client can declare, test, and review.</p><h3>Business Code Should Read Like Business Code</h3><p>A maintainable API client should let endpoint methods stay close to the domain:</p><pre>class UsersApi {<br>  UsersApi(this._client);<br><br>  final Client _client;<br>  Future&lt;Map&lt;String, Object?&gt;&gt; getUser(String id) async {<br>    final response = await _client.get(&#39;/users/$id&#39;);<br>    return await response.json&lt;Map&lt;String, Object?&gt;&gt;();<br>  }<br>}</pre><p>There is no token handling, timeout logic, retry loop, redirect handling, or status validation in that method.</p><p>That is not because those concerns disappeared. It is because they should not be reimplemented inside every endpoint method. UsersApi.getUser should describe how to call the user endpoint, not rebuild network governance on the side.</p><p>In Oxy, those defaults live in ClientOptions:</p><pre>import &#39;package:oxy/oxy.dart&#39;;<br><br>final client = Client(<br>  ClientOptions(<br>    baseUrl: Uri.parse(&#39;https://api.example.com&#39;),<br>    timeoutPolicy: const TimeoutPolicy(total: Duration(seconds: 10)),<br>    retryPolicy: const RetryPolicy(maxRetries: 2),<br>    statusPolicy: StatusPolicy.throwOnError,<br>    middleware: [<br>      RequestIdMiddleware(),<br>      AuthMiddleware.staticToken(&#39;your-token&#39;),<br>    ],<br>  ),<br>);<br><br>final users = UsersApi(client);</pre><p>This is not just “centralized configuration.” It gives code review a real boundary:</p><ul><li>Endpoint methods own path, query, payload, and decoding.</li><li>Client policies own timeout, retry, redirect, and status validation.</li><li>Middleware owns cross-cutting behavior such as auth, request IDs, cookies, and logging.</li><li>Transport owns the actual send operation and can be swapped in tests.</li></ul><p>When the boundary is clear, many bugs can be questioned during design instead of being reconstructed from logs after production.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xq8M1d8zHrD8kd09A351tw.png" /></figure><p><em>Policy-first is not about adding a configuration object. It is about moving network rules out of scattered call sites and into an explainable control plane.</em></p><h3>Policy-First Does Not Mean More Configuration</h3><p>policy-first can sound abstract. It does not mean every behavior should become a configuration knob, and it does not mean the network layer should become a framework.</p><p>It starts with a more practical observation: real API clients have rules that should exist before any individual request is written.</p><p>For example:</p><ul><li>What is the total timeout?</li><li>Should connect, send, first byte, and read timeouts be handled separately?</li><li>Which status codes are retryable?</li><li>Should retry be limited to idempotent methods?</li><li>Should redirects be followed automatically?</li><li>Should non-2xx responses throw by default or return a response?</li></ul><p>If these rules are written locally inside request functions, they are hard to test consistently and hard for a team to remember.</p><p>Oxy makes them explicit policies:</p><pre>final client = Client(<br>  ClientOptions(<br>    timeoutPolicy: const TimeoutPolicy(<br>      total: Duration(seconds: 20),<br>      firstByte: Duration(seconds: 5),<br>    ),<br>    retryPolicy: const RetryPolicy(<br>      maxRetries: 2,<br>      retryableStatusCodes: {408, 429, 500, 502, 503, 504},<br>    ),<br>    redirectPolicy: RedirectPolicy.follow,<br>    statusPolicy: StatusPolicy.throwOnError,<br>  ),<br>);</pre><p>That changes the network layer from “control flow scattered across many functions” into “a default behavior table you can actually read.”</p><p>At this point, a useful question for any project is: where do these rules live today? If the answer is “everywhere”, API client quality is already starting to drift.</p><h3>Middleware Needs to Know Its Timescale</h3><p>Middleware is one of the easiest places for a network layer to become a junk drawer.</p><p>Adding a token can be middleware. Logging can be middleware. Cookie handling can be middleware. The issue is that these behaviors do not all live on the same timescale.</p><p>Some behavior should run once for the user’s logical request:</p><ul><li>Generate a request ID.</li><li>Add an auth header.</li><li>Attach tenant, trace, locale, or other business context.</li></ul><p>Other behavior should run for every actual network attempt:</p><ul><li>Record a new attempt after a retry.</li><li>Merge cookies through a cookie jar.</li><li>Log transport-level activity.</li><li>Read attempt-level response information.</li></ul><p>If those two categories are mixed together, retry makes the behavior ambiguous. Does this middleware run once, or once per attempt?</p><p>Oxy separates the two:</p><pre>final client = Client(<br>  ClientOptions(<br>    baseUrl: Uri.parse(&#39;https://api.example.com&#39;),<br>    middleware: [<br>      RequestIdMiddleware(),<br>      AuthMiddleware.staticToken(&#39;your-token&#39;),<br>    ],<br>    networkMiddleware: [<br>      CookieMiddleware(MemoryCookieJar()),<br>      LoggingMiddleware(),<br>    ],<br>  ),<br>);</pre><p>middleware targets the logical request.</p><p>networkMiddleware targets the network attempt.</p><p>The distinction is easy to miss in a demo, but it matters in production. It keeps retry, logging, cookies, auth, and request IDs from becoming tangled in ways that no one can explain.</p><h3>Retry Is Not a For Loop</h3><p>Many retry bugs begin with the idea that retry simply means “try again after failure.”</p><pre>for (var attempt = 0; attempt &lt; 3; attempt++) {<br>  try {<br>    return await sendRequest();<br>  } catch (_) {<br>    await Future&lt;void&gt;.delayed(backoff(attempt));<br>  }<br>}</pre><p>This looks reasonable until you hit request bodies.</p><p>JSON, bytes, and form data are usually replayable. A one-shot stream is not. For example, a stream opened for a file upload may have been consumed by the first send. Retrying it does not make the request more reliable. It creates a more subtle failure.</p><p>Oxy’s Body carries replayability metadata. Retry policy does not look only at error type and status code. It also checks whether the request can be safely replayed.</p><p><em>A retry policy cannot only look at error type. It needs to understand method, status, timeout, and body replayability.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7inmrYpM06FSwP5QfBkOjg.png" /></figure><p>This is where policy-first becomes valuable: a policy is not just a configuration value. It needs to understand the structure of the request.</p><p>A reliable retry strategy needs to answer at least these questions:</p><ul><li>Is this method allowed to retry?</li><li>Is this error actually worth retrying?</li><li>Did the server send Retry-After?</li><li>Can the body be replayed?</li><li>If retry is exhausted, can the caller inspect the last error or response?</li></ul><p>If these questions are scattered through business code, it is hard to know whether every endpoint answered them correctly.</p><h3>Status Codes Should Not Become a Religious Debate</h3><p>One long-running network library debate is whether non-2xx responses should throw or return a response.</p><p>Treating that as a global philosophy question is not very useful. The real answer usually depends on endpoint semantics.</p><p>For most API clients, it is reasonable for 500, 401, 403, and 422 responses to enter a typed error path. Callers should not have to write this everywhere:</p><pre>if (response.status &gt;= 400) {<br>  // ...<br>}</pre><p>But some endpoints use 404 as normal control flow. For example: “user does not exist, return null.&quot;</p><p>Oxy throws StatusError for non-2xx responses by default, and lets a request opt out locally:</p><pre>Future&lt;Map&lt;String, Object?&gt;?&gt; findUser(String id) async {<br>  final response = await client.get(<br>    &#39;/users/$id&#39;,<br>    options: const RequestOptions(<br>      statusPolicy: StatusPolicy.returnResponse,<br>    ),<br>  );<br><br>  if (response.status == 404) {<br>    return null;<br>  }<br><br>  return await response.json&lt;Map&lt;String, Object?&gt;&gt;();<br>}</pre><p>This is more practical than arguing whether HTTP errors should always throw or always return.</p><p>Defaults serve the common case. Local policy expresses the exception. A reader can immediately see that this endpoint did not forget to handle 404. It intentionally treats 404 as a business branch.</p><h3>No-Throw Flow Should Not Duplicate Every Method</h3><p>Many network layers grow two parallel method sets:</p><ul><li>get</li><li>safeGet</li><li>post</li><li>safePost</li><li>put</li><li>safePut</li></ul><p>That feels clear at first, but it turns into a method matrix over time. Every new HTTP method or parameter has to be considered twice.</p><p>Oxy keeps no-throw flow behind Result:</p><pre>final result = await client.requestResult(&#39;GET&#39;, &#39;/health&#39;);<br><br>result.fold(<br>  onSuccess: (response) {<br>    print(response.status);<br>  },<br>  onFailure: (error, trace) {<br>    print(error);<br>  },<br>);</pre><p>If you already have a Request, you can use client.sendResult(...). For one-off requests, there is also top-level fetchResult(...).</p><p>The important part is not the name Result. It is avoiding a parallel API surface. Throwing and no-throw flows should share the same request model, policies, middleware, and transport instead of evolving separately.</p><h3>Testing Should Not Require a Server</h3><p>The test experience of an API client determines whether it actually gets tested.</p><p>If testing an endpoint wrapper requires starting a local server, managing ports, and handling lifecycle cleanup, many teams will only test the happy path, or skip the test entirely. Not because they do not care about tests, but because the friction is too high.</p><p>Oxy includes MockTransport, so tests can stop at the API client boundary:</p><pre>import &#39;package:oxy/oxy.dart&#39;;<br>import &#39;package:oxy/testing.dart&#39;;<br><br>final transport = MockTransport((request, context) async {<br>  if (request.uri.path == &#39;/users/42&#39;) {<br>    return Response.json({&#39;id&#39;: &#39;42&#39;, &#39;name&#39;: &#39;Ada&#39;});<br>  }<br>  return Response.json({&#39;error&#39;: &#39;not found&#39;}, status: 404);<br>});<br>final client = Client(<br>  ClientOptions(<br>    baseUrl: Uri.parse(&#39;https://api.example.com&#39;),<br>    transport: transport,<br>  ),<br>);<br>final users = UsersApi(client);<br>final user = await users.getUser(&#39;42&#39;);<br>print(user[&#39;name&#39;]); // Ada</pre><p>That kind of test can verify what actually matters:</p><ul><li>Did the endpoint request the correct path?</li><li>Were query, headers, and body built correctly?</li><li>Did 404, 500, and timeout paths behave as expected?</li><li>Did middleware participate in the request?</li><li>Did retry actually happen, and how many times?</li><li>Was a non-replayable body prevented from retrying?</li></ul><p>None of that requires a real HTTP server.</p><p>More importantly, the test is no longer only proving that a server can return data. It is proving that the API client contract is stable.</p><h3>Oxy Is Not Trying to Replace Every HTTP Client</h3><p>If you are writing a small script that sends two requests, package:http is more direct.</p><p>If your project is already deeply built around Dio’s ecosystem, interceptor model, upload/download features, and conventions, there is no reason to migrate just for novelty.</p><p>Oxy is a better fit when:</p><ul><li>You are building a Dart or Flutter SDK.</li><li>You maintain a set of app-specific API clients.</li><li>You want native and Web to share the same public API.</li><li>You want retry, timeout, redirect, and status validation to have explicit defaults.</li><li>You want errors to be typed models rather than scattered exceptions and strings.</li><li>You want deterministic API client tests without a real server.</li><li>You do not want the network layer split across multiple adapter packages.</li></ul><p>That is the position Oxy should be understood in today: not a feature-heavy HTTP client, but a small core for Dart and Flutter API clients.</p><p>Its public types intentionally avoid branded names:</p><ul><li>Client</li><li>Request</li><li>Response</li><li>Headers</li><li>Body</li><li>Policy</li><li>Middleware</li><li>Context</li></ul><p>The goal is for the network layer to look like the shape it naturally needs, not like a new world invented by a library.</p><h3>Trust Is the Hardest Part of a Foundation Library</h3><p>An HTTP client sits at the bottom of business flows. If it fails, it can affect login, payments, sync, uploads, background jobs, and the stability of the entire app.</p><p>That is why choosing a library like this is rarely only about whether the API feels nice. The real questions are:</p><ul><li>Is the direction clear?</li><li>Does the library know what it is not trying to solve?</li><li>Are error boundaries explainable?</li><li>Is the test model lightweight enough?</li><li>Will the core abstractions change names again in the next release?</li></ul><p>After Oxy 0.3.0, the focus is narrower: <strong>a policy-first HTTP client for Dart and Flutter API clients</strong>.</p><p>That means the most valuable work is not piling on more features. It is making the core more trustworthy:</p><ul><li>Better cookbooks.</li><li>More realistic SDK and API client examples.</li><li>Clearer Web/native behavior documentation.</li><li>More edge-case tests.</li><li>A clearer migration and stability story.</li></ul><p>Developers will not trust an HTTP client because of one article. The best an article can do is make the problem concrete, show the design tradeoffs, and let readers decide whether the library is solving a problem they actually have.</p><p>That is what this article is trying to do.</p><h3>A Checklist You Can Take Away</h3><p>Even if you never use Oxy, these questions are useful for reviewing your own Dart or Flutter network layer:</p><ul><li>Does every endpoint follow a timeout policy, or does each function improvise?</li><li>Is non-2xx handling consistent?</li><li>Does retry understand method, status, error type, and body replayability?</li><li>Do auth, request ID, cookie, and logging middleware run at the logical request level or the network attempt level?</li><li>Can callers distinguish timeout, status, decode, cancellation, and network failures?</li><li>Do API client tests require starting a server?</li><li>Are Web/native differences hidden behind a transport boundary?</li><li>Is business code still repeating network governance?</li></ul><p>If you can answer these clearly, your network layer already has a strong foundation.</p><p>If the answer is often “it depends on the function”, you may not need another helper. You may need a more stable API client path.</p><h3>Closing</h3><p>HTTP requests are easy.</p><p>The hard part is making the twentieth, fiftieth, and hundredth request keep the same timeout, retry, status policy, middleware behavior, and test model.</p><p>Oxy is not trying to answer “how do I send a request?” It is trying to answer “how do I keep a set of Dart and Flutter API clients maintainable over time?”</p><p>If you are cleaning up a Dart or Flutter network layer, or building an SDK, you can take a look at Oxy:</p><ul><li>pub.dev: <a href="https://pub.dev/packages/oxy">https://pub.dev/packages/oxy</a></li><li>GitHub: <a href="https://github.com/medz/oxy">https://github.com/medz/oxy</a></li></ul><p>I would also like to know: what is the most painful part of your Dart or Flutter network layer today? Retry, testing, Web/native differences, error modeling, or long-term API client maintenance?</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=686849ed2b0f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A New Revolution in Flutter State Management: Oref, Making Your Code Magically Reactive]]></title>
            <link>https://shiwei.medium.com/a-new-revolution-in-flutter-state-management-oref-making-your-code-magically-reactive-1a211538a989?source=rss-64442dec557e------2</link>
            <guid isPermaLink="false">https://medium.com/p/1a211538a989</guid>
            <category><![CDATA[reactive-programming]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[state-management]]></category>
            <category><![CDATA[dart]]></category>
            <category><![CDATA[oref]]></category>
            <dc:creator><![CDATA[Seven Du]]></dc:creator>
            <pubDate>Fri, 03 Oct 2025 15:48:12 GMT</pubDate>
            <atom:updated>2025-10-03T16:17:48.103Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oRCLftuUn0o1MurLR-Ct2Q.png" /></figure><blockquote>In the world of Flutter development, state management is like that thorny bramble — it should be simple, yet it always drags you into a swamp of boilerplate code. Provider? Too many listeners. Bloc? Events feel like playing Tetris. Riverpod? Elegant, but you still have to learn a bunch of new concepts. Have you ever wondered if state management could “magically” inject into your Widgets like Vue.js, with just one line of code automatically responding to changes?</blockquote><p>Hi, I’m Seven, a Flutter-obsessed developer. Today, I want to take you into Oref — a reactive state management library based on signals. It’s not just another “framework,” but a lightweight “magic wand” that frees you from the chains of StatefulWidget, letting you focus on writing clean, fast code. It’s 2025 — time to upgrade your toolkit.</p><h3>The Pain Points of State Management: Why Are We Always Struggling in “Refactoring Hell”?</h3><p>Recall: You just finished a counter Widget, but adding an async API call forces you to refactor into StatefulWidget, add setState, and listen for changes everywhere. Or with Provider, you have to wrap layers of Consumer, making the code bloat like Russian nesting dolls. The result? Performance bottlenecks, debugging nightmares, and endless “why not refactor?” meetings.</p><p>The Flutter community has tried various solutions: from the primitive setState era, to BLoC’s event-driven approach, to Riverpod’s dependency injection. But the core issue remains: too much boilerplate code, and reactivity that’s too passive. Oref is here, disrupting it all with a “signals” pattern — inspired by Solid.js and Vue’s reactivity system, but tailored specifically for Flutter.</p><h3>What is Oref? A Zero-Boilerplate, Ultra-High-Performance Signals Library</h3><p>Oref is built on <a href="https://pub.dev/packages/alien_signals"><strong>alien_signals</strong></a>, providing signals, computed values, and effects. Its killer feature? Context injection: Signals “magically” bind directly to BuildContext without hooks or mixins. Your StatelessWidget instantly becomes a reactive powerhouse.</p><ul><li>Core Philosophy: Non-intrusive. Oref doesn’t force you to rewrite existing code; it integrates like air.</li><li>Performance Champion: With optimizations from alien_signals, Oref excels in rebuild and response speeds, especially for high-frequency updates. Early user feedback shows it significantly reduces memory usage and rebuild overhead in real apps.</li><li>Async-Friendly: Built-in useAsyncData supports Future/Stream, seamlessly handling loading states.</li></ul><p>Installation is super simple:</p><pre>flutter pub add oref</pre><h3>Hands-On Example: From Zero to Hero in Just 10 Lines of Code</h3><p>Let’s demonstrate with a classic counter. A traditional Provider version might take 50+ lines; Oref? Check this out:</p><pre>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:oref/oref.dart&#39;;<br><br>void main() =&gt; runApp(MyApp());<br><br>class MyApp extends StatelessWidget {<br>  @override<br>  Widget build(BuildContext context) {<br>    final count = signal(context, 0); // Create a sugnals<br>    final increment = () =&gt; count(count() + 1); // increment count<br><br>    return MaterialApp(<br>      home: Scaffold(<br>        body: Center(<br>          child: Text(&#39;Count: ${count()}&#39;), // read the count value<br>        ),<br>        floatingActionButton: FloatingActionButton(<br>          onPressed: increment,<br>          child: Icon(Icons.add),<br>        ),<br>      ),<br>    );<br>  }<br>}</pre><p>Run it, tap the button — the count updates automatically, no setState or Consumer needed!</p><p>Magic, right? Signals batch changes in the background, rebuilding only what’s necessary. Want full examples? <a href="https://github.com/medz/oref/tree/main/example">GitHub example folder</a> is ready for you to fork.</p><p>Oref isn’t meant to replace everything — it’s complementary to flutter_hooks. If you love template-style, try flutter_signals; but if you crave minimalism, Oref is your new favorite.</p><h3>Future Outlook: Oref’s Signals Will Illuminate Flutter</h3><p>Flutter’s future is reactive — Oref is just the starting point. Try it out, and share your “magic moments” in the comments. Who knows, your App might be the next App Store star.</p><p>Like, share, subscribe — let’s make state management fun together!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1a211538a989" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Dart develops web applications, Spry may be better.]]></title>
            <link>https://shiwei.medium.com/dart-develops-web-applications-spry-may-be-better-346163962cf6?source=rss-64442dec557e------2</link>
            <guid isPermaLink="false">https://medium.com/p/346163962cf6</guid>
            <category><![CDATA[dart]]></category>
            <category><![CDATA[shelves]]></category>
            <category><![CDATA[spry]]></category>
            <category><![CDATA[web]]></category>
            <category><![CDATA[flutter]]></category>
            <dc:creator><![CDATA[Seven Du]]></dc:creator>
            <pubDate>Tue, 27 Dec 2022 06:44:58 GMT</pubDate>
            <atom:updated>2022-12-27T07:01:47.829Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HHQaC3D1JC5HwlL6FHQZww.png" /></figure><h3>What is Spry？</h3><p>Spry is an HTTP middleware framework for Dart to make web applications and APIs more enjoyable to write.</p><h3>How is it different from Shelf?</h3><ol><li>Middleware — Shelf middleware requires a complete call process (this is the source of my pain), and Spry absorbs the experience of Express/Koa, passing the “next” method to let the middleware decide when to call the next middleware.</li><li>Context — The Shelf context is attached to the request object and Spry wraps the request/response and whatever else with the context.</li><li>Request — Spry has made some practical packages for requests, which can facilitate the expansion of middleware, but it is not very different from Shelf. The only difference is that Spry’s request is in read-only mode. Middleware can be modified by context attachment.</li><li>Response — Shelf’s response is a read-only object. To modify it in the pipeline (such as middleware), you can only recreate the Response. Spry is different, the Spry response is created with the context and can be modified fluently.</li><li>Handlers — Shelf’s handler only accepts a request object, and then decides what type of Response to return. Spry accepts a context object, which contains the processing results of the pre-middleware, and also wraps the request and response. You can Modify the response anywhere until it is sent.</li></ol><h3>Middleware</h3><p>A shelf middleware:</p><pre>FutureOr&lt;Response&gt; middleware(Handler handler) {<br>  return (Request request) {<br>    // Front TODO<br>    <br>    final response = handler(request);<br><br>    // Rear TODO<br><br>    // Must return a response<br>    return response.<br>  }<br>}</pre><p>The Spry middleware:</p><pre>middleware(Context context, MiddlewareNext next) async {<br>  // Front TODO ...<br>  await next();<br>  // Rear TODO ...<br>})</pre><p>Everyone will feel different about which is better, but Spry simplifies the return process of middleware and the complexity of function implementation.</p><h3>Request handler</h3><p>Spry define a handler:</p><pre>handler(Context context) {<br>  // TODO<br>}</pre><p>Shelf handler:</p><pre>FutureOr&lt;Response&gt; handler(Request request) {<br>  // TODO<br>  return Response.ok(...);<br>}</pre><p>You can see the difference, Spry does not force the processing to create a response, because the response already exists when the context is created. There can be no logic in the handler.</p><h3>Router</h3><p>Let’s take `/hello/{name}` as an example, in Shelf:</p><pre>router.get(&#39;/hello/&lt;name&gt;&#39;, handler);</pre><p>In Spry:</p><pre>router.get(&#39;/hello/:name&#39;, handler);</pre><p>There is no difference in this basic route matching except the syntax is different, but Spry supports more advanced route matching modes.</p><p>Now we come to a complex route:</p><pre>// Spry<br>router.get(&#39;/hello/:name*&#39;, handler);<br><br>// Shelf<br>router.get(&#39;/hello/&lt;name|.*&gt;&#39;, handler);</pre><p>The route matching mode of Spry router is similar to that of Express/Koa. Thanks to the practice of the NodeJS ecosystem for many years, we believe that path to regular expression is a very mature way with the least burden on developers.</p><h3>Router mount / Subroute</h3><p>The Spry sub-route mount is fully adaptive. We are not allowed to modify the path of the request object to simulate this process. The full path should be returned to the developer!</p><p>The shelf sub-routing completes the matching by modifying request.uri.path, and Spry determines the prefix at the routing stage to complete it!</p><p>Example:</p><pre>// Spry<br>router.mount(prefix: &#39;/hello&#39;, (Context context) {<br>  // TODO.<br>});<br><br>// Shelf<br>router.mount(&#39;/hello&#39;, (Request request) {<br>  // TODO<br><br>  return response;<br>})</pre><p>In the above example, both sides are easy to use, what if we add path rules to prefix?</p><pre>// Spry<br>router.mount(prefix: &#39;/hello/:name&#39;, (Context context) {<br>  // TODO.<br>})<br><br>// Shelf<br>// not support</pre><p>Here’s another example:</p><pre>// Spry<br>final Router api = Router(&#39;/api&#39;)<br><br>api.get(&#39;/users/:id&#39;, handler); // full path is &quot;/api/users/:id&quot;<br><br>router.mount(api);<br><br>// Shelf<br>final Router api = Router();<br><br>api.get(&#39;/users/:id&#39;, handler); // full path is /users/:id<br><br>router.mount(&#39;/api&#39;, router); first match `/api/*`<br></pre><p>In this example, route grouping is carried out by mounting sub-routes, which is not supported by shelf.</p><h3>More…</h3><p>If you think spry is ok. Lightly give spry a 🌟star.</p><p>GitHub: <a href="https://github.com/odroe/spry">https://github.com/odroe/spry</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=346163962cf6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Heroicons for Vue.js — Free, Open source icons]]></title>
            <link>https://shiwei.medium.com/heroicons-for-vue-js-free-open-source-icons-d7b31f0f770a?source=rss-64442dec557e------2</link>
            <guid isPermaLink="false">https://medium.com/p/d7b31f0f770a</guid>
            <category><![CDATA[vue]]></category>
            <category><![CDATA[heroicons]]></category>
            <dc:creator><![CDATA[Seven Du]]></dc:creator>
            <pubDate>Thu, 23 Apr 2020 05:15:14 GMT</pubDate>
            <atom:updated>2020-04-23T05:19:17.178Z</atom:updated>
            <content:encoded><![CDATA[<h3>Heroicons for Vue.js — Free, Open source icons</h3><p>A set of free MIT-licensed high-quality SVG icons for Vue.js development.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Qq_BtlwG6p3iUEgPuzFELQ.png" /></figure><h3>Install</h3><ul><li>npm i @bytegem/vue-heroicons -S</li><li>yarn add @bytegem/vue-heroicons</li></ul><h3>Using</h3><pre>import Vue from &#39;vue&#39;;<br>import Heroicons from &#39;@bytegem/vue-heroicons&#39;;</pre><pre>Vue.use(Heroicons)</pre><h3>Example</h3><pre>&lt;HeroIconAdjustmentsOutline /&gt;</pre><p>Or</p><pre>&lt;hero-icon-adjustments-outline /&gt;</pre><p>GitGub: <a href="https://github.com/bytegem/vue-heroicons">https://github.com/bytegem/vue-heroicons</a></p><p>Docs website: <a href="https://bytegem.github.io/vue-heroicons/">https://bytegem.github.io/vue-heroicons/</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d7b31f0f770a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ThinkSNS+ is the use of Laravel framework to achieve the user ecosystem, Can be friendly and low…]]></title>
            <link>https://shiwei.medium.com/thinksns-is-the-use-of-laravel-framework-to-achieve-the-user-ecosystem-can-be-friendly-and-low-eb49d62e4ff1?source=rss-64442dec557e------2</link>
            <guid isPermaLink="false">https://medium.com/p/eb49d62e4ff1</guid>
            <category><![CDATA[laravel]]></category>
            <category><![CDATA[composer]]></category>
            <category><![CDATA[php]]></category>
            <dc:creator><![CDATA[Seven Du]]></dc:creator>
            <pubDate>Sat, 29 Jul 2017 13:48:17 GMT</pubDate>
            <atom:updated>2017-07-29T13:48:17.953Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/128/1*R7DjeNKQQi7ak4cyn1Jkfg.png" /><figcaption>ThinkSNS+</figcaption></figure><p><a href="https://github.com/slimkit/thinksns-plus">ThinkSNS+</a> is the use of <a href="https://github.com/laravel/laravel">Laravel</a> framework to achieve the user ecosystem, Can be friendly and low coupling development development applications.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eb49d62e4ff1" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>