<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shingo Kawamura</title>
    <description>The latest articles on DEV Community by Shingo Kawamura (@pannakoota).</description>
    <link>https://dev.to/pannakoota</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F132222%2Fd309af95-2ce1-4acb-8a99-922fb3c3f78a.png</url>
      <title>DEV Community: Shingo Kawamura</title>
      <link>https://dev.to/pannakoota</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pannakoota"/>
    <language>en</language>
    <item>
      <title>CPAN Rescue: Growing Maintainers, Not Collecting Modules</title>
      <dc:creator>Shingo Kawamura</dc:creator>
      <pubDate>Thu, 01 Oct 2026 10:27:03 +0000</pubDate>
      <link>https://dev.to/pannakoota/cpan-rescue-growing-maintainers-not-collecting-modules-3p56</link>
      <guid>https://dev.to/pannakoota/cpan-rescue-growing-maintainers-not-collecting-modules-3p56</guid>
      <description>&lt;p&gt;CPAN has been around for more than 30 years.&lt;/p&gt;

&lt;p&gt;That means something interesting has started to happen.&lt;/p&gt;

&lt;p&gt;Some modules are still useful.&lt;br&gt;&lt;br&gt;
Some are still deeply embedded in production systems.&lt;br&gt;&lt;br&gt;
Some sit underneath dozens, hundreds, or thousands of other distributions.&lt;/p&gt;

&lt;p&gt;But the people who originally maintained them may have moved on.&lt;/p&gt;

&lt;p&gt;The code remains.&lt;/p&gt;

&lt;p&gt;The dependency remains.&lt;/p&gt;

&lt;p&gt;The maintenance responsibility does not automatically move with it.&lt;/p&gt;

&lt;p&gt;That is the problem I started &lt;strong&gt;CPAN Rescue&lt;/strong&gt; to explore.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cpan-rescue.com" rel="noopener noreferrer"&gt;https://cpan-rescue.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At first, the idea was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Find useful CPAN distributions that are abandoned or under-maintained, help repair them, and adopt them when appropriate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After doing the work, I realized that rescuing modules is not the real problem.&lt;/p&gt;

&lt;p&gt;The harder problem is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How do we create the next generation of open-source maintainers?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Software can outlive its maintainers
&lt;/h2&gt;

&lt;p&gt;A library does not need frequent releases to be valuable.&lt;/p&gt;

&lt;p&gt;Sometimes the opposite is true.&lt;/p&gt;

&lt;p&gt;A module may be ten or fifteen years old because its API is stable and it simply works.&lt;/p&gt;

&lt;p&gt;That does not mean nobody depends on it.&lt;/p&gt;

&lt;p&gt;A quiet dependency can still sit underneath:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;production applications,&lt;/li&gt;
&lt;li&gt;developer tools,&lt;/li&gt;
&lt;li&gt;build systems,&lt;/li&gt;
&lt;li&gt;database clients,&lt;/li&gt;
&lt;li&gt;command-line utilities,&lt;/li&gt;
&lt;li&gt;or hundreds of other packages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But maintainers are human.&lt;/p&gt;

&lt;p&gt;People change jobs.&lt;/p&gt;

&lt;p&gt;They become busy.&lt;/p&gt;

&lt;p&gt;Their interests change.&lt;/p&gt;

&lt;p&gt;They burn out.&lt;/p&gt;

&lt;p&gt;They move to another ecosystem.&lt;/p&gt;

&lt;p&gt;Eventually, a project can reach a strange state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Code: still useful
Users: still present
Dependencies: still present
Maintainer: unavailable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, the problem is not merely "old code."&lt;/p&gt;

&lt;p&gt;It is a &lt;strong&gt;stewardship problem&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  My first instinct was: just maintain more modules
&lt;/h2&gt;

&lt;p&gt;That seems like the obvious solution.&lt;/p&gt;

&lt;p&gt;If a useful CPAN distribution needs help, someone can adopt it.&lt;/p&gt;

&lt;p&gt;So I started doing exactly that.&lt;/p&gt;

&lt;p&gt;CPAN Rescue has already reached a few concrete outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;distributions have been adopted,&lt;/li&gt;
&lt;li&gt;an upstream pull request has been merged,&lt;/li&gt;
&lt;li&gt;a post-adoption CPAN release has been published,&lt;/li&gt;
&lt;li&gt;and maintenance work has included regression testing, artifact validation, and downstream compatibility checks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One example is &lt;strong&gt;Devel::CallChecker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is a low-level compatibility module used around Perl call-checking APIs, including XS code.&lt;/p&gt;

&lt;p&gt;This is not the kind of module where I wanted to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Its own test suite passes, so let's ship it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For infrastructure code, that is not enough.&lt;/p&gt;

&lt;p&gt;The work involved reconstructing the current baseline, understanding existing behavior, checking metadata and release artifacts, and testing downstream distributions so that existing failures could be distinguished from regressions introduced by a new release.&lt;/p&gt;

&lt;p&gt;That experience reinforced something important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance is a skill of judgment, not just a skill of coding.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But it also exposed a flaw in the original idea.&lt;/p&gt;

&lt;p&gt;If I adopt 10 modules, I become responsible for 10 modules.&lt;/p&gt;

&lt;p&gt;If I adopt 100 modules, I become responsible for 100 modules.&lt;/p&gt;

&lt;p&gt;That does not solve the bus-factor problem.&lt;/p&gt;

&lt;p&gt;It just moves the bus factor to me.&lt;/p&gt;




&lt;h2&gt;
  
  
  So CPAN Rescue changed direction
&lt;/h2&gt;

&lt;p&gt;The project is still about maintaining real CPAN software.&lt;/p&gt;

&lt;p&gt;But adoption is no longer the primary goal.&lt;/p&gt;

&lt;p&gt;The goal is now:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use real maintenance work to grow maintainers.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I want someone to be able to enter the project without already knowing how CPAN maintenance works.&lt;/p&gt;

&lt;p&gt;They should not need a PAUSE account.&lt;/p&gt;

&lt;p&gt;They should not need release experience.&lt;/p&gt;

&lt;p&gt;They should not need to promise to maintain a distribution forever.&lt;/p&gt;

&lt;p&gt;Instead, responsibility can grow gradually.&lt;/p&gt;

&lt;p&gt;The path currently looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explorer
   ↓
Contributor
   ↓
Release Contributor
   ↓
Co-maintainer
   ↓
Maintainer / Steward
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a path, not a ranking.&lt;/p&gt;

&lt;p&gt;Nobody is required to reach the final stage.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does a first maintenance task look like?
&lt;/h2&gt;

&lt;p&gt;The first task should be small.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verify the current CPAN release,&lt;/li&gt;
&lt;li&gt;identify the canonical source repository,&lt;/li&gt;
&lt;li&gt;reproduce a reported problem,&lt;/li&gt;
&lt;li&gt;run the existing tests on a modern Perl,&lt;/li&gt;
&lt;li&gt;add regression coverage,&lt;/li&gt;
&lt;li&gt;repair CI,&lt;/li&gt;
&lt;li&gt;inspect generated metadata,&lt;/li&gt;
&lt;li&gt;test a release tarball in a clean environment,&lt;/li&gt;
&lt;li&gt;or classify downstream test failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are real maintenance tasks.&lt;/p&gt;

&lt;p&gt;They are not exercises invented for training.&lt;/p&gt;

&lt;p&gt;That matters.&lt;/p&gt;

&lt;p&gt;A toy project can teach syntax.&lt;/p&gt;

&lt;p&gt;It cannot easily teach questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this failure caused by our patch or was it already present?&lt;/li&gt;
&lt;li&gt;Is the repository actually the source of the CPAN release?&lt;/li&gt;
&lt;li&gt;Does this behavioral change violate an old compatibility promise?&lt;/li&gt;
&lt;li&gt;Should this module be revived at all?&lt;/li&gt;
&lt;li&gt;How much downstream testing is appropriate?&lt;/li&gt;
&lt;li&gt;Should we adopt it, co-maintain it, fund it, or migrate users away from it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are maintainer questions.&lt;/p&gt;

&lt;p&gt;And the only good way I know to learn them is by maintaining real software.&lt;/p&gt;




&lt;h2&gt;
  
  
  Release engineering is part of maintainership
&lt;/h2&gt;

&lt;p&gt;Writing the patch is often the easy part.&lt;/p&gt;

&lt;p&gt;For important infrastructure, a maintainer also needs to understand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source
  ↓
build
  ↓
release artifact
  ↓
distribution metadata
  ↓
upload
  ↓
indexing
  ↓
CPAN Testers
  ↓
downstream behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A release can be technically valid while still being unsafe for the ecosystem.&lt;/p&gt;

&lt;p&gt;That is especially true for low-level modules with many reverse dependencies.&lt;/p&gt;

&lt;p&gt;So CPAN Rescue tries to make release validation part of the learning process.&lt;/p&gt;

&lt;p&gt;A contributor may help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build and inspect a release artifact,&lt;/li&gt;
&lt;li&gt;test representative downstream distributions,&lt;/li&gt;
&lt;li&gt;compare candidate behavior with the previous release,&lt;/li&gt;
&lt;li&gt;review CPAN Testers results,&lt;/li&gt;
&lt;li&gt;or investigate regressions after release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should not need release credentials just to learn release engineering.&lt;/p&gt;

&lt;p&gt;That boundary is important.&lt;/p&gt;




&lt;h2&gt;
  
  
  Maintainers also need to know how to leave
&lt;/h2&gt;

&lt;p&gt;There is another side of open-source maintenance that we do not talk about enough.&lt;/p&gt;

&lt;p&gt;We often explain how to become a maintainer.&lt;/p&gt;

&lt;p&gt;We rarely explain how to stop being one.&lt;/p&gt;

&lt;p&gt;That creates an unhealthy implication:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Once you accept responsibility, you own it forever.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No human system should work that way.&lt;/p&gt;

&lt;p&gt;So CPAN Rescue treats handoff as part of normal stewardship.&lt;/p&gt;

&lt;p&gt;A healthy maintainer should be able to leave behind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a clear canonical repository,&lt;/li&gt;
&lt;li&gt;a reproducible release process,&lt;/li&gt;
&lt;li&gt;the current release state,&lt;/li&gt;
&lt;li&gt;known regressions,&lt;/li&gt;
&lt;li&gt;important downstream risks,&lt;/li&gt;
&lt;li&gt;CI and release requirements,&lt;/li&gt;
&lt;li&gt;open work,&lt;/li&gt;
&lt;li&gt;and a clear transfer of repository and PAUSE permissions where appropriate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If nobody is available to take over, that fact should be visible.&lt;/p&gt;

&lt;p&gt;Stepping back should not be considered failure.&lt;/p&gt;

&lt;p&gt;A project that cannot survive the departure of one person was never sustainable in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Not every old module needs to be rescued
&lt;/h2&gt;

&lt;p&gt;This is another lesson I learned quickly.&lt;/p&gt;

&lt;p&gt;An old release date is not evidence of abandonment.&lt;/p&gt;

&lt;p&gt;Some modules are simply finished.&lt;/p&gt;

&lt;p&gt;Others may have an active maintainer who does not release often.&lt;/p&gt;

&lt;p&gt;And sometimes the correct answer is not maintenance at all.&lt;/p&gt;

&lt;p&gt;For a given distribution, the ecosystem may be better served by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserving it,&lt;/li&gt;
&lt;li&gt;finding a co-maintainer,&lt;/li&gt;
&lt;li&gt;funding the existing maintainer,&lt;/li&gt;
&lt;li&gt;improving CI,&lt;/li&gt;
&lt;li&gt;documenting a migration path,&lt;/li&gt;
&lt;li&gt;replacing it with a maintained alternative,&lt;/li&gt;
&lt;li&gt;or deliberately doing nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means "number of adopted modules" is a terrible success metric.&lt;/p&gt;

&lt;p&gt;If adoption itself becomes the goal, the project starts optimizing for ownership instead of ecosystem health.&lt;/p&gt;

&lt;p&gt;I do not want CPAN Rescue to become a collection of modules.&lt;/p&gt;




&lt;h2&gt;
  
  
  What should success look like?
&lt;/h2&gt;

&lt;p&gt;Technical outcomes still matter.&lt;/p&gt;

&lt;p&gt;I want to track things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bugs fixed,&lt;/li&gt;
&lt;li&gt;regressions prevented,&lt;/li&gt;
&lt;li&gt;releases published,&lt;/li&gt;
&lt;li&gt;upstream patches merged,&lt;/li&gt;
&lt;li&gt;reverse dependencies protected,&lt;/li&gt;
&lt;li&gt;and abandoned distributions returned to a maintainable state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But those are not the metrics I care about most.&lt;/p&gt;

&lt;p&gt;The more important questions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many people completed a real maintenance task?&lt;/li&gt;
&lt;li&gt;How many participated in release validation?&lt;/li&gt;
&lt;li&gt;How many became co-maintainers?&lt;/li&gt;
&lt;li&gt;How many eventually made an independent release?&lt;/li&gt;
&lt;li&gt;How many maintainers were able to hand responsibility to someone else safely?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project becomes truly interesting when this happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CPAN Rescue
    ↓
new contributor
    ↓
release contributor
    ↓
co-maintainer
    ↓
maintainer
    ↓
teaches another contributor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, maintenance capacity is reproducing itself.&lt;/p&gt;

&lt;p&gt;That is much more valuable than one person maintaining a large number of modules.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why CPAN?
&lt;/h2&gt;

&lt;p&gt;CPAN is a particularly interesting place to test this idea.&lt;/p&gt;

&lt;p&gt;It is old enough to contain multiple generations of software.&lt;/p&gt;

&lt;p&gt;It contains modules that are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;new,&lt;/li&gt;
&lt;li&gt;stable,&lt;/li&gt;
&lt;li&gt;forgotten,&lt;/li&gt;
&lt;li&gt;deeply depended upon,&lt;/li&gt;
&lt;li&gt;historically important,&lt;/li&gt;
&lt;li&gt;actively maintained,&lt;/li&gt;
&lt;li&gt;or quietly embedded in systems nobody wants to rewrite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it a useful laboratory for a problem that every mature package ecosystem eventually faces.&lt;/p&gt;

&lt;p&gt;The details differ, but the underlying questions are familiar across ecosystems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CPAN        → Perl
PyPI        → Python
RubyGems    → Ruby
npm         → JavaScript
crates.io   → Rust
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Who maintains infrastructure after the original author leaves?&lt;/p&gt;

&lt;p&gt;How do new people acquire enough context to take responsibility safely?&lt;/p&gt;

&lt;p&gt;How do we reduce bus factor without creating another overloaded maintainer?&lt;/p&gt;

&lt;p&gt;How do we preserve institutional knowledge that currently exists only inside someone's head?&lt;/p&gt;

&lt;p&gt;How should maintenance be funded without letting sponsors buy technical control?&lt;/p&gt;

&lt;p&gt;These are not Perl-specific questions.&lt;/p&gt;

&lt;p&gt;CPAN is simply where I am starting.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI may make this problem more important, not less
&lt;/h2&gt;

&lt;p&gt;AI is making code generation cheaper.&lt;/p&gt;

&lt;p&gt;It can already help with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reading unfamiliar code,&lt;/li&gt;
&lt;li&gt;generating tests,&lt;/li&gt;
&lt;li&gt;updating CI,&lt;/li&gt;
&lt;li&gt;investigating APIs,&lt;/li&gt;
&lt;li&gt;writing documentation,&lt;/li&gt;
&lt;li&gt;and proposing patches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is useful.&lt;/p&gt;

&lt;p&gt;But maintenance contains another category of work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should this change be released?&lt;/p&gt;

&lt;p&gt;Is this API behavior actually a bug?&lt;/p&gt;

&lt;p&gt;Is maintaining this package better than migrating users away?&lt;/p&gt;

&lt;p&gt;How much downstream evidence is enough?&lt;/p&gt;

&lt;p&gt;Who should receive release authority?&lt;/p&gt;

&lt;p&gt;When should compatibility win over cleanup?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are stewardship decisions.&lt;/p&gt;

&lt;p&gt;If producing code becomes dramatically easier, the bottleneck may increasingly move from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Who can write this?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Who understands the consequences well enough to take responsibility for it?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That makes maintainer education more important, not less.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sustainability also means funding
&lt;/h2&gt;

&lt;p&gt;There is another direction I want CPAN Rescue to explore.&lt;/p&gt;

&lt;p&gt;Some infrastructure is important enough that volunteer maintenance alone may not be a sustainable answer.&lt;/p&gt;

&lt;p&gt;In those cases, the right intervention may involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grants,&lt;/li&gt;
&lt;li&gt;recurring sponsorship,&lt;/li&gt;
&lt;li&gt;compatibility infrastructure,&lt;/li&gt;
&lt;li&gt;paid maintenance capacity,&lt;/li&gt;
&lt;li&gt;secondary reviewers,&lt;/li&gt;
&lt;li&gt;or fiscal hosting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But funding introduces its own governance problem.&lt;/p&gt;

&lt;p&gt;A sponsor should be able to support maintenance without purchasing technical authority over a shared project.&lt;/p&gt;

&lt;p&gt;So one of the questions I am beginning to explore is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can we fund maintenance capacity while keeping technical governance maintainer-led?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Again, I do not know the answer yet.&lt;/p&gt;

&lt;p&gt;That is part of the experiment.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real hypothesis
&lt;/h2&gt;

&lt;p&gt;CPAN Rescue is still young.&lt;/p&gt;

&lt;p&gt;I do not want to present it as a solved model.&lt;/p&gt;

&lt;p&gt;The central idea is still a hypothesis:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can real maintenance work be turned into a practical path for growing the next generation of open-source maintainers?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is yes, the important output will not be the number of modules rescued.&lt;/p&gt;

&lt;p&gt;It will be the people who become capable of maintaining software responsibly.&lt;/p&gt;

&lt;p&gt;And the best possible outcome would be slightly paradoxical:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CPAN Rescue should eventually work without me.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the project depends permanently on its founder, then it has reproduced exactly the problem it is trying to solve.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I want to leave behind
&lt;/h2&gt;

&lt;p&gt;The name "CPAN Rescue" makes it sound like the goal is to save old Perl modules.&lt;/p&gt;

&lt;p&gt;That is part of the work.&lt;/p&gt;

&lt;p&gt;But it is not what I ultimately want to preserve.&lt;/p&gt;

&lt;p&gt;I want to help build a culture where software can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;understood,&lt;/li&gt;
&lt;li&gt;maintained,&lt;/li&gt;
&lt;li&gt;released,&lt;/li&gt;
&lt;li&gt;reviewed,&lt;/li&gt;
&lt;li&gt;handed over,&lt;/li&gt;
&lt;li&gt;and eventually entrusted to someone new.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not forever by one maintainer.&lt;/p&gt;

&lt;p&gt;But across generations of maintainers.&lt;/p&gt;

&lt;p&gt;If we can demonstrate that model in CPAN, perhaps some of the lessons will be useful elsewhere too.&lt;/p&gt;

&lt;p&gt;The project is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kawamurashingo/cpan-rescue" rel="noopener noreferrer"&gt;https://github.com/kawamurashingo/cpan-rescue&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you maintain CPAN distributions, depend on older Perl infrastructure, or are interested in open-source sustainability and maintainer succession, I would be very interested in your feedback.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>perl</category>
      <category>softwareengineering</category>
      <category>community</category>
    </item>
    <item>
      <title>I built a transport-independent foundation for JSON API clients in Perl</title>
      <dc:creator>Shingo Kawamura</dc:creator>
      <pubDate>Sat, 19 Sep 2026 23:37:59 +0000</pubDate>
      <link>https://dev.to/pannakoota/i-built-a-transport-independent-foundation-for-json-api-clients-in-perl-1od6</link>
      <guid>https://dev.to/pannakoota/i-built-a-transport-independent-foundation-for-json-api-clients-in-perl-1od6</guid>
      <description>&lt;p&gt;Writing a simple API client is easy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$http&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the client starts being used in production.&lt;/p&gt;

&lt;p&gt;Suddenly, it needs to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;query parameters&lt;/li&gt;
&lt;li&gt;JSON encoding and decoding&lt;/li&gt;
&lt;li&gt;structured errors&lt;/li&gt;
&lt;li&gt;retries and &lt;code&gt;Retry-After&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;rate limits&lt;/li&gt;
&lt;li&gt;pagination&lt;/li&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;request IDs&lt;/li&gt;
&lt;li&gt;logging and metrics&lt;/li&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, what started as a small API wrapper often becomes a collection of infrastructure code.&lt;/p&gt;

&lt;p&gt;I found myself looking at this problem and asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much of this is actually specific to the API being wrapped?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not very much.&lt;/p&gt;

&lt;p&gt;That led me to build &lt;strong&gt;HTTP::API::Core&lt;/strong&gt;, a small, dependency-light foundation for building JSON HTTP API clients in Perl.&lt;/p&gt;

&lt;p&gt;The key idea is simple: &lt;strong&gt;keep HTTP transport separate from API-client policy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use HTTP::Tiny, LWP, Mojo::UserAgent, Furl, or your preferred transport for HTTP. Let HTTP::API::Core handle the repetitive policy around it.&lt;/p&gt;

&lt;p&gt;The project is available on &lt;a href="https://github.com/kawamurashingo/HTTP-API-Core" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  It is deliberately not another HTTP client
&lt;/h2&gt;

&lt;p&gt;One of the main design decisions was not to replace the existing HTTP ecosystem.&lt;/p&gt;

&lt;p&gt;Perl already has good HTTP implementations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP::Tiny&lt;/li&gt;
&lt;li&gt;LWP&lt;/li&gt;
&lt;li&gt;Mojo::UserAgent&lt;/li&gt;
&lt;li&gt;Furl&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HTTP::API::Core sits above the transport layer.&lt;/p&gt;

&lt;p&gt;The HTTP implementation is responsible for making the request.&lt;/p&gt;

&lt;p&gt;HTTP::API::Core is responsible for the application-level policies surrounding that request.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your API client
      |
HTTP::API::Core
      |
HTTP::Tiny / LWP / Mojo / Furl / custom transport
      |
     HTTP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation means an API wrapper does not have to couple its retry, pagination, error handling, or authentication logic to a particular HTTP library.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small example
&lt;/h2&gt;

&lt;p&gt;A client can start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;HTTP::API::&lt;/span&gt;&lt;span class="nv"&gt;Core&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;HTTP::API::&lt;/span&gt;&lt;span class="nv"&gt;Core&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
    &lt;span class="s"&gt;headers&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;Authorization&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;$ENV&lt;/span&gt;&lt;span class="s2"&gt;{API_TOKEN}&lt;/span&gt;&lt;span class="p"&gt;",&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;attempts&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;base_delay&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;max_delay&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;jitter&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$api&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="p"&gt;');&lt;/span&gt;
&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A service-specific client can then remain small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="nf"&gt;get_user&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;@_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$self&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;get&lt;/span&gt;&lt;span class="p"&gt;("&lt;/span&gt;&lt;span class="s2"&gt;/users/&lt;/span&gt;&lt;span class="si"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;")&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resource method describes the API.&lt;/p&gt;

&lt;p&gt;The core handles the repetitive infrastructure around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries are more complicated than they look
&lt;/h2&gt;

&lt;p&gt;Retries were one of the areas where I wanted conservative defaults.&lt;/p&gt;

&lt;p&gt;HTTP::API::Core retries safe/idempotent methods by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET
HEAD
PUT
DELETE
OPTIONS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not automatically retry &lt;code&gt;POST&lt;/code&gt; or &lt;code&gt;PATCH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Retryable failures include transport failures and HTTP responses such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;408
425
429
5xx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also understands &lt;code&gt;Retry-After&lt;/code&gt; and rate-limit reset metadata.&lt;/p&gt;

&lt;p&gt;Delays use exponential backoff with optional jitter.&lt;/p&gt;

&lt;p&gt;If an API explicitly supports idempotent POST requests, that behavior can be enabled rather than silently assumed.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nv"&gt;$api&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;post&lt;/span&gt;&lt;span class="p"&gt;('&lt;/span&gt;&lt;span class="s1"&gt;/jobs&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
    &lt;span class="s"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;methods&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;['&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;'],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is to make the safe behavior the boring default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limits should be data, not special cases
&lt;/h2&gt;

&lt;p&gt;Different APIs expose rate limits differently.&lt;/p&gt;

&lt;p&gt;Instead of making application code repeatedly inspect raw headers, responses expose normalized rate-limit information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;rate_limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="nv"&gt;$rate&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;limit&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt; &lt;span class="nv"&gt;$rate&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="nv"&gt;$rate&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;remaining&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt; &lt;span class="nv"&gt;$rate&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="nv"&gt;$rate&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;wait_seconds&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;$rate&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;exhausted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP::API::Core understands the commonly used &lt;code&gt;X-RateLimit-*&lt;/code&gt; family as well as standard-style rate-limit fields and &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The retry system can use the same normalized information.&lt;/p&gt;

&lt;p&gt;This is one of the ideas behind the project: parse infrastructure information once and expose it consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  One pagination interface, different API styles
&lt;/h2&gt;

&lt;p&gt;Pagination is another thing that looks trivial until you support several APIs.&lt;/p&gt;

&lt;p&gt;Some APIs return a next URL.&lt;/p&gt;

&lt;p&gt;Others use page numbers.&lt;/p&gt;

&lt;p&gt;Others use opaque cursors.&lt;/p&gt;

&lt;p&gt;HTTP::API::Core provides one iterator interface for these styles.&lt;/p&gt;

&lt;p&gt;For cursor pagination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$pager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$api&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
    &lt;span class="s"&gt;mode&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
    &lt;span class="s"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data.users&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
    &lt;span class="k"&gt;next&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meta.next_cursor&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
    &lt;span class="s"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For page-number pagination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$pager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$api&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
    &lt;span class="s"&gt;mode&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
    &lt;span class="s"&gt;items&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;
    &lt;span class="s"&gt;page_size&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;@users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pager&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeated cursors or next URLs are detected so a broken API response does not accidentally create an infinite pagination loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication is implemented through hooks
&lt;/h2&gt;

&lt;p&gt;I did not want authentication to become a growing collection of special cases inside the HTTP client.&lt;/p&gt;

&lt;p&gt;Instead, HTTP::API::Core provides lifecycle hooks.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="nv"&gt;$api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;HTTP::API::&lt;/span&gt;&lt;span class="nv"&gt;Core&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com&lt;/span&gt;&lt;span class="p"&gt;',&lt;/span&gt;

    &lt;span class="s"&gt;hooks&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;before_request&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;@_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="nv"&gt;$ctx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;}{&lt;/span&gt;&lt;span class="nv"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;";&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;

        &lt;span class="s"&gt;after_response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;@_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="nv"&gt;log_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;

        &lt;span class="s"&gt;on_error&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sub &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;my&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;@_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="nv"&gt;record_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$error&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;category&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same mechanism can be used for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication&lt;/li&gt;
&lt;li&gt;logging&lt;/li&gt;
&lt;li&gt;metrics&lt;/li&gt;
&lt;li&gt;tracing&lt;/li&gt;
&lt;li&gt;request customization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small helpers for Bearer, Basic, and API-key authentication are included.&lt;/p&gt;

&lt;p&gt;OAuth token acquisition and refresh deliberately remain outside the core.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability without choosing an observability framework
&lt;/h2&gt;

&lt;p&gt;The library records useful request metadata without requiring a specific logging or tracing stack.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;say&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;request_id&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common request ID headers are normalized, including:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-Request-Id
Request-Id
X-Correlation-Id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hooks can then send this information wherever an application wants.&lt;/p&gt;

&lt;p&gt;The library provides the data without deciding how the application should observe it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured errors
&lt;/h2&gt;

&lt;p&gt;Another design goal was to avoid application code parsing human-readable error strings.&lt;/p&gt;

&lt;p&gt;Errors have machine-readable categories such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;encode
decode
transport
http
hook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Application code can inspect fields including:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="nv"&gt;$error&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$error&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$error&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;retryable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$error&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP errors retain their response information, so callers can still inspect headers, text, JSON, or rate-limit state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transport independence
&lt;/h2&gt;

&lt;p&gt;The transport boundary is intentionally small.&lt;/p&gt;

&lt;p&gt;A transport receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;\&lt;/span&gt;&lt;span class="nv"&gt;%options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and returns a response-like hash containing at least:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight perl"&gt;&lt;code&gt;&lt;span class="s"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the same API client policy can sit on top of different HTTP implementations.&lt;/p&gt;

&lt;p&gt;It also makes testing API-specific code possible without requiring real network requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing against real API conventions
&lt;/h2&gt;

&lt;p&gt;The repository includes example clients demonstrating different API styles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub — page-number pagination and rate-limit metadata&lt;/li&gt;
&lt;li&gt;Slack — cursor pagination&lt;/li&gt;
&lt;li&gt;Cloudflare — page-number pagination using response metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not intended to replace their official SDKs.&lt;/p&gt;

&lt;p&gt;They are examples showing that the abstraction can map onto APIs with significantly different conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I intentionally left out
&lt;/h2&gt;

&lt;p&gt;A reusable core can easily become a framework that tries to solve everything.&lt;/p&gt;

&lt;p&gt;I am trying to avoid that.&lt;/p&gt;

&lt;p&gt;HTTP::API::Core intentionally does not handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;service-specific SDK behavior&lt;/li&gt;
&lt;li&gt;complete OAuth flows&lt;/li&gt;
&lt;li&gt;OpenAPI generation&lt;/li&gt;
&lt;li&gt;GraphQL-specific behavior&lt;/li&gt;
&lt;li&gt;WebSockets&lt;/li&gt;
&lt;li&gt;HTTP server functionality&lt;/li&gt;
&lt;li&gt;async runtime concerns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is intended to remain small, predictable, dependency-light, and transport-independent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader idea
&lt;/h2&gt;

&lt;p&gt;Although this implementation is for Perl, the architectural question is language-independent.&lt;/p&gt;

&lt;p&gt;Where should the boundary be between:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the HTTP transport,&lt;/li&gt;
&lt;li&gt;reusable API-client policy, and&lt;/li&gt;
&lt;li&gt;service-specific domain methods?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Putting everything into the HTTP layer makes the transport too opinionated.&lt;/p&gt;

&lt;p&gt;Putting everything into every service SDK means repeatedly implementing retries, pagination, errors, authentication, and rate-limit handling.&lt;/p&gt;

&lt;p&gt;HTTP::API::Core is my attempt to put that boundary in the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;HTTP::API::Core is available on GitHub:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/kawamurashingo/HTTP-API-Core" rel="noopener noreferrer"&gt;github.com/kawamurashingo/HTTP-API-Core&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd be especially interested in feedback from people who maintain API clients or SDKs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What functionality do you consider generic API-client infrastructure, and what would you deliberately keep out of a core like this?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>perl</category>
      <category>api</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I Built a jq-Compatible Tool in Pure Perl (and Why It Still Matters)</title>
      <dc:creator>Shingo Kawamura</dc:creator>
      <pubDate>Fri, 02 Jan 2026 13:06:54 +0000</pubDate>
      <link>https://dev.to/pannakoota/why-i-built-a-jq-compatible-tool-in-pure-perl-and-why-it-still-matters-45a6</link>
      <guid>https://dev.to/pannakoota/why-i-built-a-jq-compatible-tool-in-pure-perl-and-why-it-still-matters-45a6</guid>
      <description>&lt;p&gt;Modern tooling often assumes a perfect world:&lt;br&gt;
latest Linux, internet access, package managers, and rebuildable binaries.&lt;/p&gt;

&lt;p&gt;Reality is different.&lt;/p&gt;

&lt;p&gt;In many production environments — especially enterprise, legacy, or air-gapped systems —&lt;br&gt;
those assumptions break down.&lt;/p&gt;

&lt;p&gt;That gap is why I built jq-lite.&lt;/p&gt;




&lt;p&gt;The Problem: jq Is Great — Until You Can’t Use It&lt;/p&gt;

&lt;p&gt;jq is an excellent JSON processor.&lt;br&gt;
But in real operations, I kept hitting walls:&lt;/p&gt;

&lt;p&gt;Legacy UNIX systems with outdated glibc&lt;/p&gt;

&lt;p&gt;Air-gapped environments with no package repositories&lt;/p&gt;

&lt;p&gt;Minimal containers without build tools&lt;/p&gt;

&lt;p&gt;Environments where “just install jq” was not an option&lt;/p&gt;

&lt;p&gt;In those cases, JSON processing didn’t disappear —&lt;br&gt;
the need became more critical.&lt;/p&gt;

&lt;p&gt;Shell scripts still had to be reliable.&lt;br&gt;
Automation still had to work.&lt;/p&gt;




&lt;p&gt;The Idea: A jq-Compatible Tool That Never Breaks Scripts&lt;/p&gt;

&lt;p&gt;I didn’t want another JSON tool.&lt;/p&gt;

&lt;p&gt;I wanted a tool that could be:&lt;/p&gt;

&lt;p&gt;Dropped into restricted systems&lt;/p&gt;

&lt;p&gt;Trusted in shell scripts&lt;/p&gt;

&lt;p&gt;Stable across years, not releases&lt;/p&gt;

&lt;p&gt;So I built jq-lite with three strict rules:&lt;/p&gt;




&lt;p&gt;Design Rule #1: Pure Perl, No Dependencies&lt;/p&gt;

&lt;p&gt;jq-lite is written in pure Perl.&lt;/p&gt;

&lt;p&gt;No native extensions&lt;/p&gt;

&lt;p&gt;No external libraries&lt;/p&gt;

&lt;p&gt;No compilation step&lt;/p&gt;

&lt;p&gt;If Perl exists, jq-lite runs.&lt;/p&gt;

&lt;p&gt;That means it works on:&lt;/p&gt;

&lt;p&gt;legacy Linux / UNIX&lt;/p&gt;

&lt;p&gt;minimal containers&lt;/p&gt;

&lt;p&gt;offline environments&lt;/p&gt;

&lt;p&gt;systems where only base OS tools are allowed&lt;/p&gt;

&lt;p&gt;Perl is still one of the most widely deployed runtimes in the world —&lt;br&gt;
especially in places modern tooling forgot.&lt;/p&gt;




&lt;p&gt;Design Rule #2: jq Compatibility Where It Matters&lt;/p&gt;

&lt;p&gt;jq-lite aims to be jq-compatible in daily CLI usage:&lt;/p&gt;

&lt;p&gt;echo '{}' | jq-lite --arg greeting hello '.hello = $greeting'&lt;/p&gt;

&lt;p&gt;Output:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "hello": "hello"&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The goal isn’t to clone every feature,&lt;br&gt;
but to support the jq patterns people actually use in automation.&lt;/p&gt;




&lt;p&gt;Design Rule #3: A Stable CLI Contract (This Is the Important Part)&lt;/p&gt;

&lt;p&gt;Here’s the part that made jq-lite different.&lt;/p&gt;

&lt;p&gt;jq-lite defines a documented CLI contract:&lt;/p&gt;

&lt;p&gt;exit codes&lt;/p&gt;

&lt;p&gt;error categories&lt;/p&gt;

&lt;p&gt;stderr prefixes&lt;/p&gt;

&lt;p&gt;stdout behavior on failure&lt;/p&gt;

&lt;p&gt;And that contract is tested.&lt;/p&gt;

&lt;p&gt;Exit Code   Meaning&lt;/p&gt;

&lt;p&gt;0   Success&lt;br&gt;
1   -e used and result was false / null / empty&lt;br&gt;
2   Compile error&lt;br&gt;
3   Runtime error&lt;br&gt;
4   Input error&lt;br&gt;
5   Usage error&lt;/p&gt;

&lt;p&gt;This means shell scripts can safely rely on jq-lite:&lt;/p&gt;

&lt;p&gt;if jq-lite -e '.enabled' config.json; then&lt;br&gt;
  deploy&lt;br&gt;
fi&lt;/p&gt;

&lt;p&gt;No surprises.&lt;br&gt;
No silent behavior changes.&lt;/p&gt;

&lt;p&gt;In operations, this matters more than features.&lt;/p&gt;




&lt;p&gt;Why Not Rewrite It in Rust or Go?&lt;/p&gt;

&lt;p&gt;I get this question a lot.&lt;/p&gt;

&lt;p&gt;Because the problem wasn’t performance or language fashion.&lt;/p&gt;

&lt;p&gt;The problem was deployability and longevity.&lt;/p&gt;

&lt;p&gt;Perl already exists on:&lt;/p&gt;

&lt;p&gt;old servers&lt;/p&gt;

&lt;p&gt;enterprise systems&lt;/p&gt;

&lt;p&gt;restricted environments&lt;/p&gt;

&lt;p&gt;Adding jq-lite doesn’t require changing the environment —&lt;br&gt;
just using what’s already there.&lt;/p&gt;




&lt;p&gt;Who Is jq-lite For?&lt;/p&gt;

&lt;p&gt;jq-lite is not for everyone.&lt;/p&gt;

&lt;p&gt;It is for people who:&lt;/p&gt;

&lt;p&gt;maintain legacy systems&lt;/p&gt;

&lt;p&gt;write shell scripts that must survive years&lt;/p&gt;

&lt;p&gt;operate in restricted or offline environments&lt;/p&gt;

&lt;p&gt;care about CLI behavior as a contract, not a suggestion&lt;/p&gt;

&lt;p&gt;If you’ve ever thought&lt;br&gt;
“this script must not break in five years”&lt;br&gt;
— jq-lite is for you.&lt;/p&gt;




&lt;p&gt;Where to Find It&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/kawamurashingo/JQ-Lite" rel="noopener noreferrer"&gt;https://github.com/kawamurashingo/JQ-Lite&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CLI Contract: &lt;a href="https://github.com/kawamurashingo/JQ-Lite/blob/main/docs/cli-contract.md" rel="noopener noreferrer"&gt;https://github.com/kawamurashingo/JQ-Lite/blob/main/docs/cli-contract.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CPAN: &lt;a href="https://metacpan.org/release/JQ-Lite" rel="noopener noreferrer"&gt;https://metacpan.org/release/JQ-Lite&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alpine Linux package available&lt;/p&gt;




&lt;p&gt;Final Thought&lt;/p&gt;

&lt;p&gt;Modern tools optimize for speed of change.&lt;/p&gt;

&lt;p&gt;jq-lite optimizes for stability over time.&lt;/p&gt;

&lt;p&gt;Sometimes, that’s the more important optimization.&lt;/p&gt;




&lt;p&gt;Author&lt;br&gt;
川村慎吾（Shingo Kawamura）&lt;br&gt;
SRE / Infrastructure Engineer&lt;br&gt;
CPAN author of jq-lite&lt;/p&gt;

</description>
      <category>perl</category>
      <category>devops</category>
      <category>cli</category>
      <category>legacy</category>
    </item>
  </channel>
</rss>
