<?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: PaulRichez</title>
    <description>The latest articles on DEV Community by PaulRichez (@paulrichez).</description>
    <link>https://dev.to/paulrichez</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%2F4035753%2F975622b2-a0ae-4f03-abe8-2715a7e553f0.png</url>
      <title>DEV Community: PaulRichez</title>
      <link>https://dev.to/paulrichez</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paulrichez"/>
    <language>en</language>
    <item>
      <title>grep finds the string, not the reference — Strapi's magic-string problem</title>
      <dc:creator>PaulRichez</dc:creator>
      <pubDate>Sat, 18 Jul 2026 19:40:32 +0000</pubDate>
      <link>https://dev.to/paulrichez/grep-finds-the-string-not-the-reference-strapis-magic-string-problem-5fmk</link>
      <guid>https://dev.to/paulrichez/grep-finds-the-string-not-the-reference-strapis-magic-string-problem-5fmk</guid>
      <description>&lt;p&gt;I renamed a Strapi service a while back and then spent the next hour chasing down all the call-sites I'd just broken. My editor didn't flag a single one. Grep found some, missed others, and buried the real hits under a pile of unrelated matches. That afternoon is basically why I ended up building the tool this post is about.&lt;/p&gt;

&lt;p&gt;But forget the tool for a second. Try this on your own project first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What code uses &lt;code&gt;api::article.article&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go answer it. You'll reach for grep, and grep will let you down in two different ways at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  grep finds the string. It doesn't find the reference.
&lt;/h2&gt;

&lt;p&gt;Let me be fair to grep first. If you search for the exact UID &lt;code&gt;api::article.article&lt;/code&gt;, you'll catch a lot, because it's a literal string. The relation in another content type's schema turns up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;src/api/comment/content-types/comment/schema.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"article"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"manyToOne"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api::article.article"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So does the route handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/articles/latest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api::article.article.find&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;grep finds those. Credit where it's due. The trouble starts with the references that &lt;em&gt;don't&lt;/em&gt; contain the literal, and there are two kinds that turn up constantly.&lt;/p&gt;

&lt;p&gt;The first is indirection. Pull the UID into a constant — which basically every codebase does eventually — and the call-site vanishes from your search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ARTICLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api::article.article&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nx"&gt;strapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ARTICLE&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;findLatest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// grep for the UID finds the const line, never this one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second is the method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;strapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api::article.article&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;findLatest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;//                                     ^^^^^^^^^^^ what calls findLatest()?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;findLatest&lt;/code&gt; is a needle in a haystack and grep has no clue it belongs to that service. Grep the bare name and you drown; grep the UID and you never see the method call.&lt;/p&gt;

&lt;p&gt;And even for the hits grep &lt;em&gt;does&lt;/em&gt; get, it can't tell you what they are — a relation, a service call, a route handler — or answer the questions you actually care about: what's unused, what's broken, what depends on this. That's a graph. grep does text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Okay, but I'm in TypeScript. Won't the editor find them?
&lt;/h2&gt;

&lt;p&gt;That was my assumption too. It doesn't, and the reason is oddly specific. Look at the type Strapi gives you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;strapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns &lt;code&gt;any&lt;/code&gt;. The second you type &lt;code&gt;.findLatest()&lt;/code&gt; on an &lt;code&gt;any&lt;/code&gt;, TypeScript checks out. It can't complete the method, it can't find its references, it can't rename it. Your "Find All References" comes back empty, and not because nothing calls &lt;code&gt;findLatest&lt;/code&gt; — the compiler just lost the trail the moment it hit &lt;code&gt;any&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The relation &lt;code&gt;target&lt;/code&gt; from earlier? That one's even more hopeless. It lives in a &lt;code&gt;.json&lt;/code&gt; file. There's no TypeScript in JSON to begin with, nothing to analyze, nowhere to jump.&lt;/p&gt;

&lt;p&gt;So both of the tools you'd normally trust are blind here, for different reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grep understands text but not meaning, so it over- and under-shoots.&lt;/li&gt;
&lt;li&gt;TypeScript understands types, but Strapi's magic strings live &lt;em&gt;outside&lt;/em&gt; the type system on purpose — &lt;code&gt;any&lt;/code&gt; returns, JSON schemas, string handlers in route configs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Strapi resolves all of these at runtime. Your editor resolves them never. That's the whole gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is boring: just read the schema
&lt;/h2&gt;

&lt;p&gt;Here's the thing that bugged me the most — none of this needs AI, and none of it needs codegen. The answer is already sitting in the repo. The real &lt;code&gt;schema.json&lt;/code&gt; files, the routes, the services and controllers. Something just has to &lt;em&gt;read&lt;/em&gt; them and build the reference graph, the way Strapi itself does at boot.&lt;/p&gt;

&lt;p&gt;So that's what I built. &lt;a href="https://github.com/PaulRichez/devkit-for-strapi" rel="noopener noreferrer"&gt;DevKit for Strapi&lt;/a&gt; is a VS Code extension (Cursor, Windsurf, Antigravity and VSCodium too) that reads your project's actual content-types, components, services, controllers, policies, middlewares and routes, and treats the magic strings as the real code they point at. It classifies them with the TypeScript compiler API, not regex, so it's reading structure, not pattern-matching.&lt;/p&gt;

&lt;p&gt;Autocomplete gives you the real values for wherever the cursor is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;strapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api::|&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// every real content-type UID in your project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Diagnostics underline the stuff that doesn't exist, with a "did you mean" fix:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88x0exoc1ukg64gdmvw7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88x0exoc1ukg64gdmvw7.png" alt="An invalid Strapi reference underlined, with a " width="800" height="95"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the reason I started all this — Find All References that actually finds them, including the &lt;code&gt;strapi.service('x').findLatest()&lt;/code&gt; calls and route handlers TypeScript can't see, counted right above the definition:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkeriefiy3pxvtb6jq3ee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkeriefiy3pxvtb6jq3ee.png" alt="A " width="800" height="197"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every hit gets tagged with &lt;em&gt;how&lt;/em&gt; it reaches the entity: a &lt;code&gt;service&lt;/code&gt; call, a &lt;code&gt;documents()&lt;/code&gt; call, a &lt;code&gt;db.query()&lt;/code&gt;, a &lt;code&gt;schema&lt;/code&gt; relation, a &lt;code&gt;route&lt;/code&gt; handler. That "how" is the part grep will never give you, because grep doesn't know a relation &lt;code&gt;target&lt;/code&gt; and a service call are the same reference wearing different clothes.&lt;/p&gt;

&lt;p&gt;There's the usual go-to-definition and hover on top of that, but honestly the reference graph is the piece I use every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your AI agent trips on the exact same wire
&lt;/h2&gt;

&lt;p&gt;This is the part I didn't expect until I watched it happen. If you code with Claude Code, Copilot's agent mode, or Cursor, your agent hits the same wall you do. Ask it to move or rename something and it'll grep for the references — same noise, same missed &lt;code&gt;any&lt;/code&gt;-typed calls — and then it goes ahead and &lt;em&gt;rewrites code&lt;/em&gt; off that half-right picture. That's how an agent breaks a project while looking confident about it.&lt;/p&gt;

&lt;p&gt;So the same engine ships as an MCP server, bundled in the extension (or &lt;code&gt;npx devkit-for-strapi-mcp&lt;/code&gt; if you run it standalone). The agent gets real tools instead of grep: &lt;code&gt;find_references&lt;/code&gt; and &lt;code&gt;dependents&lt;/code&gt; for who-uses-this, &lt;code&gt;get_schema&lt;/code&gt; for the actual field names, &lt;code&gt;validate_reference&lt;/code&gt; to check a UID before writing it, plus &lt;code&gt;list_broken_refs&lt;/code&gt; and the dependency graph.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7kh1deysculm9tbfopq9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7kh1deysculm9tbfopq9.png" alt="An AI agent querying the project's real values through the bundled DevKit MCP server" width="800" height="846"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A quick honesty check on the pitch: a capable agent can open your schema files and read them, so this isn't magic and I'm not going to pretend the model is helpless without me. What it actually buys you is narrower and real — one deterministic tool call instead of re-reading thirty schema files every session, and a reference graph that grep can't build whether it's you or the agent driving. The agent trips on the same &lt;code&gt;any&lt;/code&gt; return and the same constant indirection you just did.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Doesn't Strapi v5 already do this with generated types?"
&lt;/h2&gt;

&lt;p&gt;I expect this one in the comments, so let's get ahead of it. Credit where it's due: Strapi v5 added a &lt;code&gt;UID&lt;/code&gt; namespace, so when an API is typed against &lt;code&gt;UID.ContentType&lt;/code&gt;, you get autocomplete on the UID literal in your &lt;code&gt;.ts&lt;/code&gt; code. That's genuinely useful and I'm glad it exists.&lt;/p&gt;

&lt;p&gt;It still doesn't close the gap, for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;schema.json&lt;/code&gt; is JSON.&lt;/strong&gt; Generated types can't reach relation &lt;code&gt;target&lt;/code&gt;s, component references, or dynamic zones. It's structural — half your magic strings live in files TypeScript will never look at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autocompleting a literal isn't finding references.&lt;/strong&gt; TS will happily complete &lt;code&gt;'api::article.article'&lt;/code&gt; and still have no answer for "what uses it?" across the project, and no safe rename. That's a graph, not a type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The generated types go stale.&lt;/strong&gt; The typed path leans on &lt;code&gt;ts:generate-types&lt;/code&gt;, and in practice it drifts — see &lt;a href="https://github.com/strapi/strapi/issues/26348" rel="noopener noreferrer"&gt;#26348&lt;/a&gt; (types not reliably updated after you edit a content type) and &lt;a href="https://github.com/strapi/strapi/issues/18784" rel="noopener noreferrer"&gt;#18784&lt;/a&gt;. DevKit reads the live &lt;code&gt;schema.json&lt;/code&gt;, so there's no generate step to forget and nothing to go stale.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Different layer, basically. It's not "typed UIDs but nicer," it's the reference graph across JSON, route config, and &lt;code&gt;any&lt;/code&gt;-typed calls that types structurally can't touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule I stuck to: guarantee, don't guess
&lt;/h2&gt;

&lt;p&gt;If a string can't actually be proven — a variable instead of a literal, an external plugin that isn't in your workspace — DevKit says nothing rather than show you a wrong squiggle. A false positive burns trust faster than silence ever will. Unverifiable external refs get reported as &lt;code&gt;external&lt;/code&gt;, never invented.&lt;/p&gt;

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

&lt;p&gt;It's free and MIT. Works on Strapi v4 and v5 (detected per project), in JavaScript and TypeScript, no codegen, no telemetry — your code never leaves your machine.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Editor: search &lt;strong&gt;DevKit for Strapi&lt;/strong&gt; in the VS Code Marketplace or &lt;a href="https://open-vsx.org/extension/paul-richez/devkit-for-strapi" rel="noopener noreferrer"&gt;Open VSX&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Agent: &lt;code&gt;npx devkit-for-strapi-mcp&lt;/code&gt;, or just install the extension (the server's bundled)&lt;/li&gt;
&lt;li&gt;Code: &lt;strong&gt;&lt;a href="https://github.com/PaulRichez/devkit-for-strapi" rel="noopener noreferrer"&gt;https://github.com/PaulRichez/devkit-for-strapi&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finding references is free. If you want the tool to actually &lt;em&gt;change&lt;/em&gt; them for you — rename that propagates across every call-site, route handler and relation target — there's a paid tier for that. But the "stop losing fights with magic strings" part costs nothing.&lt;/p&gt;

&lt;p&gt;If you try it and it misses a pattern, or trips on some v4/v5 quirk, tell me. That's exactly the stuff I want to hear about.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure, per DEV's guidelines: I used an AI assistant to help draft and edit this post. The tool, the code, the bugs I hit, and the opinions are mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>strapi</category>
      <category>typescript</category>
      <category>vscode</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
