<?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: Theodore Kelechukwu Onyejiaku</title>
    <description>The latest articles on DEV Community by Theodore Kelechukwu Onyejiaku (@javascriptar).</description>
    <link>https://dev.to/javascriptar</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F200542%2Fe402ac5c-f2b1-413d-8bc2-9f8bc5bf24d8.jpg</url>
      <title>DEV Community: Theodore Kelechukwu Onyejiaku</title>
      <link>https://dev.to/javascriptar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/javascriptar"/>
    <language>en</language>
    <item>
      <title>What Are Agent Skills and How To Use Them</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 13 Mar 2026 09:59:47 +0000</pubDate>
      <link>https://dev.to/strapi/what-are-agent-skills-and-how-to-use-them-3jno</link>
      <guid>https://dev.to/strapi/what-are-agent-skills-and-how-to-use-them-3jno</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent Skills are an open standard for packaging reusable workflows, domain expertise, and new capabilities into portable folders that any AI agent can discover and use — think of them as "saved prompts" on steroids&lt;/li&gt;
&lt;li&gt;Skills use a three-tier progressive disclosure model to protect the context window: only the name and description load at startup (~30-50 tokens per skill), the full SKILL.md loads when triggered, and reference files load only when needed during execution&lt;/li&gt;
&lt;li&gt;Originally created by Anthropic, skills are now an open standard at agentskills.io, adopted by 26+ platforms including Claude, OpenAI Codex, Gemini CLI, GitHub Copilot, Cursor, and VS Code&lt;/li&gt;
&lt;li&gt;A skill is just a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; file (YAML frontmatter + markdown instructions) plus optional &lt;code&gt;scripts/&lt;/code&gt;, &lt;code&gt;references/&lt;/code&gt;, and &lt;code&gt;assets/&lt;/code&gt; directories — you can build one in minutes&lt;/li&gt;
&lt;li&gt;Skills are composable: combine custom skills with built-in document skills and MCP servers to build complex, predictable workflows that produce consistent output every time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Are Agent Skills?
&lt;/h2&gt;

&lt;p&gt;If you find yourself typing the same prompt across conversations — a weekly report format, a code review checklist, a data analysis pipeline — you've already felt the problem that skills solve. Agent Skills are folders of instructions that package repeated workflows, specialized knowledge, or new capabilities for your AI agent. Instead of copying and pasting lengthy prompts every session, you write them once as a skill and reuse them across every conversation.&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.amazonaws.com%2Fuploads%2Farticles%2Fgxtp3f750ziy5xf6vxzl.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.amazonaws.com%2Fuploads%2Farticles%2Fgxtp3f750ziy5xf6vxzl.png" alt="skills-overview.png" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But skills go far beyond "saved prompts." They're an &lt;strong&gt;open standard&lt;/strong&gt; originally created by Anthropic and now governed as a cross-platform specification at &lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;agentskills.io&lt;/a&gt;. &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.amazonaws.com%2Fuploads%2Farticles%2Fhtxfydvfai67htzs2vck.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.amazonaws.com%2Fuploads%2Farticles%2Fhtxfydvfai67htzs2vck.png" alt="what-ara-agent-skills.png" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On December 18, 2025, Anthropic released Agent Skills as an open standard, publishing the specification and SDK for any AI platform to adopt. This means a skill you build for Claude works identically in OpenAI Codex, Gemini CLI, GitHub Copilot, Cursor, VS Code, and over 20 other platforms that have adopted the standard.&lt;/p&gt;

&lt;p&gt;The simplest way to think about it: &lt;strong&gt;MCP gives your agent access to external tools and data. Skills teach your agent &lt;em&gt;what to do&lt;/em&gt; with those tools and data.&lt;/strong&gt;&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.amazonaws.com%2Fuploads%2Farticles%2F0ij1zxrg82xaiv0brx9p.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.amazonaws.com%2Fuploads%2Farticles%2F0ij1zxrg82xaiv0brx9p.png" alt="skill-overveiw.png" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem&lt;/strong&gt; (left) — Skills that provide instructions and domain knowledge to the agent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt; (center) — Contains the LLM and Tools working together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Servers&lt;/strong&gt; (right) — External tools and data connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subagents&lt;/strong&gt; (bottom) — Spawned by the main agent for parallel/specialized work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skills flow &lt;em&gt;into&lt;/em&gt; the agent as knowledge, MCP connects &lt;em&gt;bidirectionally&lt;/em&gt; for external capabilities, and Subagents are dispatched &lt;em&gt;from&lt;/em&gt; the agent. This visually reinforces the point: &lt;strong&gt;MCP gives your agent access to external tools and data. Skills teach your agent &lt;em&gt;what to do&lt;/em&gt; with those tools and data.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Skills Matter: From Prompts to Packages
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Prompt Repetition Problem
&lt;/h3&gt;

&lt;p&gt;Consider a content creator who publishes blog posts every week. Without skills, every conversation starts the same way:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Paste the blog post structure rules (TL;DR section, heading hierarchy, citations format)&lt;/li&gt;
&lt;li&gt;Paste the SEO guidelines (keyword density, meta description format, internal linking rules)&lt;/li&gt;
&lt;li&gt;Paste the brand voice instructions (tone, audience, formatting preferences)&lt;/li&gt;
&lt;li&gt;Paste or describe the topic and source material&lt;/li&gt;
&lt;li&gt;Hope the output actually follows all those rules consistently&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now multiply that across a team of five writers, each with their own slightly different version of those instructions. The output quality varies from session to session and person to person.&lt;/p&gt;

&lt;p&gt;This approach has three fundamental issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context window pollution&lt;/strong&gt; — Every piece of instruction you paste consumes tokens that could be used for actual content. The more instructions you front-load, the faster your context fills up and the higher the likelihood of degradation in response quality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No portability&lt;/strong&gt; — Your workflow lives in your clipboard or a shared doc somewhere, not in a versioned, shareable artifact that can be installed and used consistently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No consistency&lt;/strong&gt; — Without a standardized format, different people will interpret and paste those instructions differently, and you can never guarantee the agent follows every rule every time&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Skills Fix This
&lt;/h3&gt;

&lt;p&gt;Skills address all three problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Progressive disclosure&lt;/strong&gt; — Only the skill's name and description are loaded at startup (roughly 30-50 tokens per skill). The full instructions load only when the skill is triggered. Referenced files, scripts, and assets load only when needed during execution. You can have hundreds of skills installed without any impact on performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portability&lt;/strong&gt; — A skill is a folder. Share it as a zip file, push it to a Git repository, or publish it to a marketplace. Since it's an open standard, it works across Claude, Codex, Gemini CLI, and every other platform that supports the spec&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatability&lt;/strong&gt; — The same instructions execute the same way every time, for every team member, across every platform. In a non-deterministic system where you can never fully predict the model's output, skills provide the structure to make workflows as predictable as possible&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2F8b7gtoz1je62yzq1ngg5.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.amazonaws.com%2Fuploads%2Farticles%2F8b7gtoz1je62yzq1ngg5.png" alt="Screenshot 2026-02-06 at 1.09.49 PM.png" width="800" height="1210"&gt;&lt;/a&gt;&lt;br&gt;
Anthropic frames the context window as a &lt;strong&gt;public good&lt;/strong&gt; — the more data you add, the more tokens you consume, the faster it fills, and the greater the risk of context degradation. Progressive disclosure is what makes skills fundamentally different from just pasting a big prompt. Skills are intentional about what information goes into the context window and what stays on disk until it's actually needed.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Anatomy of a Skill
&lt;/h2&gt;

&lt;p&gt;Every skill follows the same structure, defined by the &lt;a href="https://agentskills.io/specification" rel="noopener noreferrer"&gt;Agent Skills Specification&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-skill/
├── SKILL.md              # Required — YAML frontmatter + markdown instructions
├── scripts/              # Optional — executable code (Python, Bash, JS)
├── references/           # Optional — additional documentation
└── assets/               # Optional — templates, images, logos, data files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The SKILL.md File
&lt;/h3&gt;

&lt;p&gt;This is the only required file. It has two parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. YAML Frontmatter&lt;/strong&gt; — metadata that tells the agent what this skill is and when to use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analyzing-marketing-campaign&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;-"&lt;/span&gt;
  &lt;span class="s"&gt;Analyze weekly marketing campaign performance from CSV or BigQuery data.&lt;/span&gt;
  &lt;span class="s"&gt;Use when the user asks about campaign metrics, funnel analysis, ROAS,&lt;/span&gt;
  &lt;span class="s"&gt;or budget reallocation.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; are the only two required fields. The name must be 1-64 characters, lowercase with hyphens, and should match the folder name. The description (up to 1,024 characters) is mission critical — it's how the agent detects when to activate the skill. Include trigger keywords and describe not just what the skill does, but &lt;em&gt;when&lt;/em&gt; to use it.&lt;/p&gt;

&lt;p&gt;Optional frontmatter fields include &lt;code&gt;license&lt;/code&gt;, &lt;code&gt;compatibility&lt;/code&gt; (environment requirements), &lt;code&gt;metadata&lt;/code&gt; (arbitrary key-value pairs like author and version), and &lt;code&gt;allowed-tools&lt;/code&gt; (pre-approved tool list for security-conscious environments).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Markdown Body&lt;/strong&gt; — the actual instructions, as detailed as you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Weekly Campaign Analysis&lt;/span&gt;

&lt;span class="gu"&gt;## Input Requirements&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Campaign data with columns: date, campaign_name, impressions, clicks, conversions
&lt;span class="p"&gt;-&lt;/span&gt; Date range must be specified

&lt;span class="gu"&gt;## Step 1: Data Quality Check&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Verify no missing values in required columns
&lt;span class="p"&gt;-&lt;/span&gt; Flag any anomalies (CTR &amp;gt; 20%, negative values, etc.)

&lt;span class="gu"&gt;## Step 2: Funnel Analysis&lt;/span&gt;
Calculate against these benchmarks:
| Metric | Benchmark |
|--------|-----------|
| CTR    | 2.5%      |
| CVR    | 3.0%      |

&lt;span class="gu"&gt;## Budget Reallocation&lt;/span&gt;
Only when the user asks about budget reallocation, read:
&lt;span class="sb"&gt;`references/budget_reallocation_rules.md`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that last line — the budget reallocation rules live in a separate file that &lt;strong&gt;only loads when the user asks about it&lt;/strong&gt;. That is progressive disclosure in action. Instead of cramming everything into the context window upfront, the skill tells the agent where to find additional information and when to fetch it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional Directories
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directory&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scripts/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Executable code loaded and run on demand&lt;/td&gt;
&lt;td&gt;Python data analysis, Bash automation, JS utilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Additional documentation loaded when needed&lt;/td&gt;
&lt;td&gt;Domain-specific rules, style guides, API docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assets/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Templates, images, and data files&lt;/td&gt;
&lt;td&gt;Output templates, logos, schema files, sample data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Skills aren't limited to text — they can include scripts in Python, Bash, or JavaScript that the agent executes as part of the workflow. A PDF processing skill might include Python scripts for form extraction. A data analysis skill might include diagnostic scripts and visualization code. The agent loads and runs these only when the workflow requires it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Skills Fit in the Agent Ecosystem
&lt;/h2&gt;

&lt;p&gt;With so many technologies in the AI toolkit — MCP, skills, tools, subagents — understanding when to use what is essential. Here is how they all fit together:&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.amazonaws.com%2Fuploads%2Farticles%2Fb7b815bg1a76g1cun6ck.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.amazonaws.com%2Fuploads%2Farticles%2Fb7b815bg1a76g1cun6ck.png" alt="Screenshot 2026-02-06 at 1.11.42 PM.png" width="800" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The analogy from the course captures it perfectly: &lt;strong&gt;tools are the hammer, saw, and nails. Skills are the knowledge of how to build a bookshelf.&lt;/strong&gt; The tools provide the raw capabilities (filesystem access, bash execution, API calls). Skills provide the procedural knowledge and domain expertise to use those tools in a specific, repeatable way.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Atomic unit of communication&lt;/td&gt;
&lt;td&gt;One-off instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low-level capabilities (bash, filesystem, APIs)&lt;/td&gt;
&lt;td&gt;The building blocks that power everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Connects agents to external systems and data&lt;/td&gt;
&lt;td&gt;Database access, Google Drive, Slack, any external service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skills&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Packaged workflows with domain expertise&lt;/td&gt;
&lt;td&gt;Repeatable tasks, team-wide processes, predictable output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subagents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Isolated execution with own context window&lt;/td&gt;
&lt;td&gt;Parallel tasks, specialized analysis, fine-grained permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How They Compose Together
&lt;/h3&gt;

&lt;p&gt;The real power emerges when you combine these components. Consider a workflow that chains three skills with an MCP server:&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.amazonaws.com%2Fuploads%2Farticles%2Ff3cgajeolz9bz4m18sco.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.amazonaws.com%2Fuploads%2Farticles%2Ff3cgajeolz9bz4m18sco.png" alt="Screenshot 2026-02-06 at 1.14.27 PM.png" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this flow, the &lt;strong&gt;MCP server&lt;/strong&gt; provides the connection to BigQuery. The &lt;strong&gt;marketing analysis skill&lt;/strong&gt; defines &lt;em&gt;how&lt;/em&gt; to analyze the data (benchmarks, formulas, allocation rules). The &lt;strong&gt;brand guidelines skill&lt;/strong&gt; defines &lt;em&gt;how things should look&lt;/em&gt; (colors, typography, logos). The &lt;strong&gt;built-in PowerPoint skill&lt;/strong&gt; handles the &lt;em&gt;document generation&lt;/em&gt;. Each skill loads only its own context, only when needed.&lt;/p&gt;

&lt;p&gt;You can also dispatch &lt;strong&gt;subagents&lt;/strong&gt; alongside skills. A main agent can spawn subagents — each with their own context window and specific skill access — to parallelize work. For instance, one subagent analyzes customer interviews while another processes survey data, both using the same "customer insights" skill but running in parallel. The parent agent collects and synthesizes their results.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Use What
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Reach For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Need data from an external database or API&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MCP&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need a repeatable, documented workflow&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Skills&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need to teach the agent your company's specific process&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Skills&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need parallel execution with isolated context&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Subagents&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need real-time access to Google Drive / Slack / etc.&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MCP&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need specialized analysis that shouldn't pollute main context&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Subagents + Skills&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need a one-off quick task&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Just a prompt&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use skills for procedural, predictable workflows. Use subagents for full agentic logic only when necessary for specialized or parallel tasks. Use MCP for any external data or tooling. And remember — skills, subagents, and MCP are composable. The sweet spot is usually a combination of all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built-In Skills and the Skills Repository
&lt;/h2&gt;

&lt;p&gt;Anthropic ships several production-ready skills at &lt;a href="https://github.com/anthropics/skills" rel="noopener noreferrer"&gt;github.com/anthropics/skills&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Document Skills&lt;/strong&gt; (built into Claude AI, always active):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Excel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Create, edit, and analyze &lt;code&gt;.xlsx&lt;/code&gt; spreadsheets with formatting and charts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PowerPoint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generate presentations with custom layouts, colors, typography&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Word&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Create formatted &lt;code&gt;.docx&lt;/code&gt; documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PDF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Extract text, fill forms, merge PDFs, convert to images&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Example Skills&lt;/strong&gt; (can be toggled on in Settings &amp;gt; Capabilities):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skill Creator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Meta-skill that programmatically creates new skills following best practices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code Review&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Structured code review with configurable standards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Analysis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Statistical analysis workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;skill-creator&lt;/strong&gt; is particularly useful — it's a skill that creates skills. It includes Python scripts for initializing the folder structure, packaging into a zip, and validating against best practices. It is enabled by default and can save significant time when building new skills.&lt;/p&gt;

&lt;p&gt;In Claude Code, you can install skills from the marketplace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add the Anthropic skills marketplace&lt;/span&gt;
/plugins marketplace add anthropics/skills

&lt;span class="c"&gt;# Verify installed skills&lt;/span&gt;
/skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Partner-built skills from companies like Atlassian, Figma, Canva, Stripe, Notion, and Zapier are also available through the skills directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Build a Skill: Architecture Walkthrough
&lt;/h2&gt;

&lt;p&gt;The best way to understand how all the pieces of a skill fit together is to look at a real-world example. Let's walk through the architecture of a &lt;strong&gt;Strapi Configuration Skill&lt;/strong&gt; — a skill that scaffolds fully configured Strapi CMS projects with content types, seed data, public API access, and auto-populate middleware.&lt;/p&gt;

&lt;p&gt;The goal here is not to recreate this skill step-by-step, but to understand the architectural decisions and moving parts so you can apply these patterns to your own skills. For the full source code, check out the &lt;a href="https://github.com/anthropics/skills" rel="noopener noreferrer"&gt;repository&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Directory Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strapi-configuration/
├── SKILL.md                  # Core instructions (480 lines)
├── CLAUDE.md                 # Development context for contributors
├── README.md                 # User-facing documentation
├── scripts/                  # Empty — reserved for future helpers
├── references/               # Empty — reserved for future docs
└── templates/                # Domain-specific data files
    ├── blog.json             # ~22KB preset definition
    ├── ecommerce.json        # ~23KB preset definition
    ├── portfolio.json        # ~25KB preset definition
    └── restaurant.json       # ~24KB preset definition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure shows several important patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standard directories are present even when empty.&lt;/strong&gt; The &lt;code&gt;scripts/&lt;/code&gt; and &lt;code&gt;references/&lt;/code&gt; directories exist as placeholders — they signal to other developers (and the agent) that the skill follows the standard layout and may grow into these directories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Domain-specific directories are allowed.&lt;/strong&gt; The &lt;code&gt;templates/&lt;/code&gt; directory isn't part of the standard spec (&lt;code&gt;scripts/&lt;/code&gt;, &lt;code&gt;references/&lt;/code&gt;, &lt;code&gt;assets/&lt;/code&gt; are), but skills can include any additional directories that make sense for the domain. Here, each JSON template is a ~23KB definition containing content types, components, middleware configs, seed data, and permission rules for a specific project type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progressive disclosure is baked into the architecture.&lt;/strong&gt; Those four template files total ~94KB of data. None of it loads until the agent actually needs a specific template. When the user picks "blog," only &lt;code&gt;blog.json&lt;/code&gt; enters the context — the other three templates stay on disk. That's ~70KB of tokens saved.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Frontmatter: Your Skill's Elevator Pitch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;strapi-configuration&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
  &lt;span class="s"&gt;Create a fully configured Strapi project with preconfigured content types,&lt;/span&gt;
  &lt;span class="s"&gt;seed data, public API access, and route-based middleware for default&lt;/span&gt;
  &lt;span class="s"&gt;population. Use when the user wants to scaffold a new Strapi project for&lt;/span&gt;
  &lt;span class="s"&gt;a specific use case such as blog, e-commerce, portfolio, or restaurant.&lt;/span&gt;
  &lt;span class="s"&gt;Supports preset templates and custom project descriptions.&lt;/span&gt;
&lt;span class="na"&gt;compatibility&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Requires Node.js and access to the strapi-core monorepo.&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bash Read Write Edit Glob Grep&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;strapi&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.1.0"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key decisions worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;description&lt;/strong&gt; is packed with trigger keywords: "scaffold", "Strapi project", "blog", "e-commerce", "portfolio", "restaurant", "content types", "seed data." When a user says any of these words, the agent knows this skill is relevant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;compatibility&lt;/code&gt;&lt;/strong&gt; declares the runtime requirements upfront — the agent (and the user) knows this needs Node.js before it even starts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;allowed-tools&lt;/code&gt;&lt;/strong&gt; explicitly declares which tools the skill will use. In security-conscious environments, this allows pre-authorization rather than prompting for each tool at runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Instruction Body: A Deterministic Workflow
&lt;/h3&gt;

&lt;p&gt;The body of this SKILL.md is structured as 9 sequential steps, each with clear entry/exit criteria. Here is the high-level flow:&lt;/p&gt;


  
[video-steps.mp4](https://delicate-dawn-ac25646e6d.media.strapiapp.com/video_steps_10379ccc12.mp4)
  Your browser does not support the video tag.


&lt;p&gt;Several patterns make this workflow reliable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explicit branching.&lt;/strong&gt; Step 1 presents a selection menu and routes to either Step 2 (preset template) or Step 2b (custom generation). The branching is clearly marked — the agent never has to guess which path to follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code templates, not descriptions.&lt;/strong&gt; Rather than saying "create a controller," the skill provides the exact TypeScript code:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;factories&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/strapi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCoreController&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::&amp;lt;name&amp;gt;.&amp;lt;name&amp;gt;&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;This eliminates guesswork and ensures the generated code actually works with the target framework. The skill includes templates for controllers, services, routes (with and without middleware), middleware files, component schemas, and the entire seed script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progressive file loading.&lt;/strong&gt; Step 2 says "Read the template JSON file at &lt;code&gt;templates/&amp;lt;preset&amp;gt;.json&lt;/code&gt;." It doesn't embed the template data in the SKILL.md — it tells the agent where to find it and what structure to expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;The template contains:
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`contentTypes`&lt;/span&gt; - Collection types and single types (schemas)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`components`&lt;/span&gt; - Reusable component definitions
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`middlewares`&lt;/span&gt; - Route-based populate middleware configs
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`seedData`&lt;/span&gt; - Sample data entries for each content type
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`publicPermissions`&lt;/span&gt; - Which API endpoints to make publicly accessible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The custom path generates data dynamically.&lt;/strong&gt; Step 2b is a mini-workflow within the workflow — it gathers requirements, designs a content model following patterns from existing templates, generates seed data, configures middleware, sets permissions, and optionally saves the result as a reusable template for next time. This demonstrates how skills can handle both deterministic paths (presets) and creative ones (custom generation) within the same workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supporting Files: CLAUDE.md and README.md
&lt;/h3&gt;

&lt;p&gt;This skill includes two additional files that aren't part of the agent skills spec but demonstrate good practice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/strong&gt; captures development context — the directory structure, key conventions for the Strapi schema format, CLI testing commands, common pitfalls (like &lt;code&gt;draftAndPublish&lt;/code&gt; needing to be nested inside &lt;code&gt;options&lt;/code&gt;, not at the top level), and future improvements. This is valuable for both the agent when it needs to modify the skill and for human developers maintaining it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/strong&gt; is user-facing documentation — installation instructions, quick start examples, usage for each preset, and what gets generated. It explains how to install the skill at project scope (&lt;code&gt;&amp;lt;project&amp;gt;/.claude/skills/&lt;/code&gt;) or globally (&lt;code&gt;~/.claude/skills/&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  What Makes This Skill Work Well
&lt;/h3&gt;

&lt;p&gt;Looking at the architecture holistically, several design decisions stand out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The description is rich with trigger keywords&lt;/strong&gt; — the agent can match user intent to this skill reliably&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps are numbered and sequential&lt;/strong&gt; — there is no ambiguity about execution order&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code templates are exact, not described&lt;/strong&gt; — pattern matching produces reliable output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large data lives in files, not instructions&lt;/strong&gt; — 94KB of template data stays on disk until needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branching is explicit&lt;/strong&gt; — preset vs. custom paths are clearly defined&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The skill stays under 500 lines&lt;/strong&gt; — keeping the main instructions concise and deferring detail to external files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge cases are handled&lt;/strong&gt; — what if no path is provided? What if the user picks custom? What if they want to save the result as a template?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the skill in action.&lt;/p&gt;


  
[video-steps.mp4](https://delicate-dawn-ac25646e6d.media.strapiapp.com/video_steps_10379ccc12.mp4)
  Your browser does not support the video tag.


&lt;p&gt;&lt;strong&gt;note:&lt;/strong&gt; this is still work in progress, but once it is ready, I will share the repo here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices for Building Your Own Skills
&lt;/h2&gt;

&lt;p&gt;Based on the official specification, Anthropic's engineering blog, and patterns observed across production skills:&lt;/p&gt;

&lt;h3&gt;
  
  
  Naming and Description
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: Use lowercase letters, numbers, and hyphens. Follow the &lt;code&gt;verb-ing + noun&lt;/code&gt; pattern: &lt;code&gt;analyzing-marketing-campaign&lt;/code&gt;, &lt;code&gt;generating-practice-questions&lt;/code&gt;. Don't use reserved keywords like "claude" or "anthropic." Max 64 characters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description&lt;/strong&gt;: This is how the agent decides when to use your skill. Describe both &lt;em&gt;what it does&lt;/em&gt; and &lt;em&gt;when to use it&lt;/em&gt;. Include trigger keywords. Max 1,024 characters&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Structure and Content
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep SKILL.md under 500 lines.&lt;/strong&gt; Move detailed content into &lt;code&gt;references/&lt;/code&gt;, &lt;code&gt;scripts/&lt;/code&gt;, or &lt;code&gt;assets/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use step-by-step instructions.&lt;/strong&gt; Number your steps. Specify edge cases. Be clear about what to skip and why&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provide code templates, not descriptions.&lt;/strong&gt; Agents produce more reliable output when they can pattern-match against exact examples&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use progressive disclosure aggressively.&lt;/strong&gt; If a piece of information is only needed 20% of the time, put it in a reference file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use forward slashes&lt;/strong&gt; in all file paths, even on Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Degree of Freedom
&lt;/h3&gt;

&lt;p&gt;Think about how much freedom to give the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low freedom&lt;/strong&gt; for processes that must be followed exactly (compliance checks, data pipelines, test procedures)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High freedom&lt;/strong&gt; for creative outputs where variety is desirable (design, writing, brainstorming)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Testing and Evaluation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Run your skill through the &lt;strong&gt;skill-creator&lt;/strong&gt; skill to score it against best practices&lt;/li&gt;
&lt;li&gt;Write test cases like unit tests: define input queries, expected behaviors, and output formats&lt;/li&gt;
&lt;li&gt;Test across all models you plan to use — different models may interpret instructions differently&lt;/li&gt;
&lt;li&gt;Gather human feedback and iterate&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Skills Can Live
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Project&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;project&amp;gt;/.claude/skills/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Team-specific workflows tied to a codebase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.claude/skills/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Personal skills available across all projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Settings &amp;gt; Capabilities &amp;gt; Skills&lt;/td&gt;
&lt;td&gt;Upload as zip, available in the web UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Marketplace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;anthropics/skills&lt;/code&gt; on GitHub&lt;/td&gt;
&lt;td&gt;Community and official skills&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Open Standard: Cross-Platform Portability
&lt;/h2&gt;

&lt;p&gt;Agent Skills follow the same playbook Anthropic used with the Model Context Protocol: build a specification that solves a real interoperability problem, release it as an open standard, and let ecosystem adoption create network effects.&lt;/p&gt;

&lt;p&gt;The platforms that have adopted the standard now include Claude, Claude Code, OpenAI Codex, Gemini CLI, GitHub Copilot, VS Code, Cursor, Roo Code, Amp, Goose, Mistral AI, Databricks, and many others — over 26 at last count. Partner-built skills from Atlassian, Figma, Canva, Stripe, Notion, and Zapier are available at launch.&lt;/p&gt;

&lt;p&gt;The specification and reference library live at &lt;a href="https://github.com/agentskills/agentskills" rel="noopener noreferrer"&gt;github.com/agentskills/agentskills&lt;/a&gt;. The reference library includes a validation tool you can use to check your skills against the spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;skills-ref validate ./my-skill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Getting Started: Your First Skill in 5 Minutes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Create the folder:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .claude/skills/my-first-skill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create &lt;code&gt;SKILL.md&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-first-skill&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
  &lt;span class="s"&gt;Describe what your skill does and when to trigger it.&lt;/span&gt;
  &lt;span class="s"&gt;Include keywords that users are likely to say.&lt;/span&gt;
&lt;span class="s"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# My First Skill&lt;/span&gt;

&lt;span class="gu"&gt;## Steps&lt;/span&gt;

&lt;span class="gu"&gt;### Step 1: [First thing to do]&lt;/span&gt;
[Detailed instructions...]

&lt;span class="gu"&gt;### Step 2: [Second thing to do]&lt;/span&gt;
[Detailed instructions...]

&lt;span class="gu"&gt;## Output Format&lt;/span&gt;
[What the final result should look like]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Test it&lt;/strong&gt; by starting a new session and asking a question that matches your skill's description.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Iterate.&lt;/strong&gt; Run it through the skill-creator for best-practice feedback, or validate it with the reference library. Refine based on actual usage.&lt;/p&gt;

&lt;p&gt;Whether you're automating a weekly report, encoding your team's code review process, building a project scaffolding tool, or packaging brand guidelines for consistent design output — skills transform one-off prompts into durable, shareable, composable knowledge that works everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Citations&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent Skills Specification: &lt;a href="https://agentskills.io/specification" rel="noopener noreferrer"&gt;https://agentskills.io/specification&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Introducing Agent Skills (Anthropic): &lt;a href="https://www.anthropic.com/news/skills" rel="noopener noreferrer"&gt;https://www.anthropic.com/news/skills&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Equipping Agents for the Real World with Agent Skills: &lt;a href="https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills" rel="noopener noreferrer"&gt;https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Skill Authoring Best Practices (Claude Docs): &lt;a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Agent Skills Overview (Claude API Docs): &lt;a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview" rel="noopener noreferrer"&gt;https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Agent Skills GitHub Repository: &lt;a href="https://github.com/anthropics/skills" rel="noopener noreferrer"&gt;https://github.com/anthropics/skills&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Agent Skills Open Standard Repository: &lt;a href="https://github.com/agentskills/agentskills" rel="noopener noreferrer"&gt;https://github.com/agentskills/agentskills&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI Codex Skills Documentation: &lt;a href="https://developers.openai.com/codex/skills/" rel="noopener noreferrer"&gt;https://developers.openai.com/codex/skills/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;VS Code Agent Skills: &lt;a href="https://code.visualstudio.com/docs/copilot/customization/agent-skills" rel="noopener noreferrer"&gt;https://code.visualstudio.com/docs/copilot/customization/agent-skills&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>aiskills</category>
      <category>mcp</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>How to Build a Strapi Plugin That Extends the Admin and Backend</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 13 Mar 2026 09:49:03 +0000</pubDate>
      <link>https://dev.to/strapi/how-to-build-a-strapi-plugin-that-extends-the-admin-and-backend-2enj</link>
      <guid>https://dev.to/strapi/how-to-build-a-strapi-plugin-that-extends-the-admin-and-backend-2enj</guid>
      <description>&lt;p&gt;Whether you’re building a product solo, working with clients, or maintaining a CMS for a growing team, the starting point is usually the same.&lt;/p&gt;

&lt;p&gt;You define content types, expose them through APIs, and consume that content in your application. Editors work in a structured interface, developers get predictable data, and the CMS stays focused on content.&lt;/p&gt;

&lt;p&gt;That already covers a lot — but rarely everything.&lt;/p&gt;

&lt;p&gt;Real products quickly introduce needs that go beyond content modeling: custom backend logic, admin workflows, or UI extensions that reflect how content is actually created and reviewed. When that happens, it’s important to know whether the CMS can adapt to additional requirements and scale with the product. This is about avoiding situations where small needs later force difficult architectural decisions.&lt;/p&gt;

&lt;p&gt;This is where Strapi starts to matter.&lt;/p&gt;

&lt;p&gt;Strapi is designed to be extended through plugins. Plugins are the mechanism Strapi provides to adapt the CMS to your project: adding backend logic, extending APIs, and customizing the admin interface when needed — without pushing those needs outside the CMS you already rely on.&lt;/p&gt;

&lt;p&gt;In this article, we’ll make this concrete by building a small but real Strapi plugin: a todo list that integrates directly into the admin panel and attaches itself to any content type.&lt;/p&gt;

&lt;p&gt;Let’s get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Strapi
&lt;/h2&gt;

&lt;p&gt;To get started, you'll need a Strapi project to work with. &lt;br&gt;
You can follow &lt;a href="https://docs.strapi.io/cms/quick-start" rel="noopener noreferrer"&gt;the official quick-start guide&lt;/a&gt;, or simply run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-strapi-app@latest my-strapi-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




  


&lt;p&gt;Once the installation is finished you can navigate into the newly created folder and start Strapi in development mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;my-strapi-project &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run develop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Strapi starts, it will open in your browser at &lt;a href="http://localhost:1337/admin/auth/register-adminhttp://localhost:1337/admin/auth/register-admin" rel="noopener noreferrer"&gt;http://localhost:1337/admin/auth/register-admin&lt;/a&gt;. The first screen prompts you to create an admin user for your application. Once that’s done, you’ll land in the Strapi admin and can start building.&lt;/p&gt;

&lt;p&gt;At this point, you have a working Strapi project and a clean starting point for plugin development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bootstrapping a Strapi plugin with the Plugin SDK
&lt;/h2&gt;

&lt;p&gt;After you’ve installed Strapi and created your admin user you are now ready to start developing your plugin! &lt;/p&gt;

&lt;p&gt;Strapi provides an &lt;a href="https://docs.strapi.io/cms/plugins-development/plugin-sdk" rel="noopener noreferrer"&gt;official plugin SDK&lt;/a&gt; that takes care of structure and conventions for you. From the root of your project, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @strapi/sdk-plugin init todo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command launches an interactive CLI that asks a few questions about your plugin. Based on your answers, it generates a blank plugin inside &lt;code&gt;src/plugins/todo&lt;/code&gt;.&lt;/p&gt;


  


&lt;p&gt;Once the plugin has been bootstrapped, you can enable it by updating &lt;code&gt;./config/plugins.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/plugins/todo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that in place, Strapi will load your plugin at startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building your plugin
&lt;/h3&gt;

&lt;p&gt;At this point, Strapi is running and your plugin is enabled — but there’s one important detail to be aware of before you start writing code.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;strapi develop&lt;/code&gt;, Strapi does &lt;strong&gt;not&lt;/strong&gt; compile plugin code for you. Plugin code is built separately, which means that while developing your plugin you’ll need to run an additional build process.&lt;/p&gt;

&lt;p&gt;From the plugin directory, start the watch command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;src/plugins/todo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This watches your plugin files and rebuilds them as you make changes.&lt;/p&gt;

&lt;p&gt;This separation is intentional. In Strapi, plugins are treated as &lt;strong&gt;isolated units&lt;/strong&gt;, with their own dependencies and build lifecycle, rather than as hidden parts of the main application. Keeping plugin builds explicit makes the boundary between the application and its extensions clear, and avoids coupling plugin development to the core dev process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a content type
&lt;/h3&gt;

&lt;p&gt;In Strapi, &lt;strong&gt;everything you store is a content type&lt;/strong&gt;. Whether it’s an Article, a Product, or a User profile, the data lives in a model with a schema — and Strapi uses that schema to generate the database structure, admin behavior, and API layer.&lt;/p&gt;

&lt;p&gt;A plugin doesn’t change that. Even if the UI and logic live inside the plugin, the data still needs a place to live.&lt;/p&gt;

&lt;p&gt;So if our todo plugin is going to store tasks, we need a content type for them — and we’ll define it &lt;strong&gt;inside the plugin&lt;/strong&gt;, so the whole feature (UI + backend + data model) stays self-contained.&lt;/p&gt;

&lt;p&gt;The easiest way to do this is with the Strapi generator CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run strapi generate content-type
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this command will open up another interactive CLI. In there you can specify the name of the content type as well as add some attributes. &lt;/p&gt;

&lt;p&gt;When prompted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name the content type &lt;code&gt;task&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;add attributes: &lt;code&gt;name&lt;/code&gt; (text) and &lt;code&gt;done&lt;/code&gt; (boolean)&lt;/li&gt;
&lt;li&gt;choose to add it to the existing &lt;code&gt;todo&lt;/code&gt; plugin&lt;/li&gt;
&lt;li&gt;allow the CLI to generate API-related files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don’t forget to select the option to add the model to your existing todo plugin and to bootstrap API related files.&lt;/p&gt;


  


&lt;p&gt;Once the generator finishes, you’ll see new files for the &lt;code&gt;task&lt;/code&gt; content type: a schema, service, controller, and routes — all scoped to the plugin. &lt;/p&gt;

&lt;p&gt;By default, &lt;strong&gt;every content type in Strapi is exposed through the Content API&lt;/strong&gt;. That’s usually what you want: content types are meant to be queried by frontends and external consumers.&lt;/p&gt;

&lt;p&gt;In this case, it’s not.&lt;/p&gt;

&lt;p&gt;Tasks are an &lt;strong&gt;internal, admin-only concern&lt;/strong&gt;. They exist purely to support editors while working in the admin panel, and should not be reachable from the public Content API.&lt;/p&gt;

&lt;p&gt;For that reason, we’ll move the generated routes from the Content API to the &lt;strong&gt;Admin API&lt;/strong&gt;, making the &lt;code&gt;task&lt;/code&gt; content type accessible only inside the admin context.&lt;/p&gt;

&lt;p&gt;To do that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Move the generated route file &lt;code&gt;src/plugins/todo/server/src/routes/content-api/task.ts&lt;/code&gt; to the &lt;code&gt;src/plugins/todo/server/src/routes/&lt;/code&gt;admin folder. You will then end up with the following file &lt;code&gt;src/plugins/todo/server/src/routes/admin/task.ts&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Task router
 */&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;factories&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/strapi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCoreRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;plugin::todo.task&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;ol&gt;
&lt;li&gt;Update  &lt;code&gt;src/plugins/todo/server/src/routes/admin/index.ts&lt;/code&gt; file to register those routes as admin routes
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;routes&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;ol&gt;
&lt;li&gt;Update  &lt;code&gt;src/plugins/todo/server/src/routes/content-api/index.ts&lt;/code&gt; to replace the content API routes with an empty definition
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;routes&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;This keeps the task API private to the admin context, which is exactly what we want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known issue with generated routes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When generating content types with the CLI, you may run into a small TypeScript edge case when registering admin routes.&lt;/p&gt;

&lt;p&gt;You might see an error like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;ERROR] server/src/routes/admin/index.ts:4:15 - TS2488:
Type&lt;span class="s1"&gt;'Route[] | (() =&amp;gt; Route[])'&lt;/span&gt; must have a&lt;span class="s1"&gt;'[Symbol.iterator]()'&lt;/span&gt; method that returns an iterator.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a typing issue only — the routes work correctly at runtime. You can safely fix it by adding a &lt;code&gt;@ts-expect-error&lt;/code&gt; when spreading the routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;exportdefault &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="c1"&gt;// @ts-expect-error&lt;/span&gt;
&lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;routes&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;h3&gt;
  
  
  Polymorphic relation
&lt;/h3&gt;

&lt;p&gt;Each task should be associated with whatever content entry it belongs to — articles, pages, users, or any other type. To support that, the relation needs to be &lt;em&gt;polymorphic&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In this context, &lt;em&gt;polymorphic&lt;/em&gt; simply means the relation isn’t tied to one specific content type. A task can point to different kinds of entries, depending on where it’s created in the admin panel.&lt;/p&gt;

&lt;p&gt;Update the &lt;code&gt;src/plugins/todo/server/src/content-types/task/schema.json&lt;/code&gt; schema to add a &lt;code&gt;morphToMany&lt;/code&gt; relation called &lt;code&gt;related&lt;/code&gt;,&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"collectionType"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"collectionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"info"&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;"singularName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"pluralName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Task"&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;"options"&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;"draftAndPublish"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;"pluginOptions"&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;"content-manager"&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;"visible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;"content-type-builder"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"visible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&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;"name"&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;"text"&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;"done"&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;"boolean"&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;"related"&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;"morphToMany"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the &lt;code&gt;task&lt;/code&gt; content type has three fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;done&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;related&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s all we need.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Fetching related tasks with a custom admin API route&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Now we need a way to fetch all tasks linked to the entry currently being edited. The catch is that Strapi doesn’t support filtering on polymorphic relations out of the box (see &lt;a href="https://docs.strapi.io/cms/faq#can-you-filter-andor-deep-filter-on-dynamic-zones-and-polymorphic-relations" rel="noopener noreferrer"&gt;the FAQ&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;So we’ll add a custom service method that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;looks up task IDs in the polymorphic relation table, then&lt;/li&gt;
&lt;li&gt;fetches the matching task documents.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Open &lt;code&gt;server/src/services/task.ts&lt;/code&gt; and extend the core service with &lt;code&gt;findRelatedTasks&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Task service
*/&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;factories&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/strapi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCoreService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;plugin::todo.task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,({&lt;/span&gt; &lt;span class="nx"&gt;strapi&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findRelatedTasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;relatedId&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="na"&gt;relatedType&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;taskIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;strapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tasks_related_mph&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;task_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;related_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;relatedId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;related_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;relatedType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;return&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;documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;plugin::todo.task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taskIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;task_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="nx"&gt;task_id&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;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;Next, expose this method through a custom controller action. Open &lt;code&gt;server/src/controllers/task.ts&lt;/code&gt; and add &lt;code&gt;findRelatedTasks&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Task controller
 */&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;factories&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/strapi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;factories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCoreController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;plugin::todo.task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,({&lt;/span&gt; &lt;span class="nx"&gt;strapi&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="nf"&gt;asyncfindRelatedTasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;relatedId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relatedType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;plugin::todo.task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findRelatedTasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;relatedId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relatedType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tasks&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;Finally, register an admin route that calls this controller. Update &lt;code&gt;server/src/routes/admin/index.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="c1"&gt;// @ts-expect-error&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&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="na"&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;/tasks/related/:relatedType/:relatedId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&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;task.findRelatedTasks&lt;/span&gt;&lt;span class="dl"&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;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place, the admin panel can request related tasks for the current entry using a single endpoint — which we’ll use later when building the sidebar UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Cleaning up the admin UI&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Now that the backend pieces are in place, it’s time to prepare the admin side.&lt;/p&gt;

&lt;p&gt;The generated content type appears in the Content Manager and Content-Type Builder by default.&lt;/p&gt;

&lt;p&gt;Because tasks are meant to be managed through the plugin UI (not as standalone entries), we’ll hide the &lt;code&gt;Task&lt;/code&gt; content type from the Content Manager and the Content-Type Builder. &lt;a href="https://docs.strapi.io/cms/plugins-development/guides/create-components-for-plugins#making-components-visible-in-the-admin-panel" rel="noopener noreferrer"&gt;You can do this by updating the schema of the Task content type at &lt;code&gt;src/plugins/todo/server/src/content-types/task/schema.json&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pluginOptions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-manager&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type-builder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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 plugin also comes with its own menu entry and page. Since this plugin only adds functionality &lt;em&gt;inside&lt;/em&gt; the Content Manager, you can remove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the plugin icon by removing &lt;code&gt;src/plugins/todo/admin/src/components/PluginIcon.tsx&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;the plugin pages by deleting &lt;code&gt;src/plugins/todo/admin/src/pages/&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;the corresponding menu registration from the &lt;code&gt;src/plugins/todo/admin/src/index.ts&lt;/code&gt; file&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Creating the sidebar panel
&lt;/h3&gt;

&lt;p&gt;The core of this plugin lives in the Content Manager edit view. We’ll add a custom sidebar component that will appear within the edit screen of a content document and will list all the tasks related to it. &lt;/p&gt;

&lt;p&gt;To do so, you’ll have to create a new component in your plugin. We’ll create that at &lt;code&gt;src/plugins/todo/admin/src/components/TodoPanel.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unstable_useContentManagerContext&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;useContentManagerContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PanelComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/content-manager/strapi-admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TodoPanel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PanelComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Todo list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; All my todos will be here. &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TodoPanel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we’ve created the panel component we can now register it using the &lt;code&gt;addEditViewSidePanel&lt;/code&gt; API provided by the Content Manager plugin. We do this in the bootstrap function of &lt;code&gt;src/plugins/todo/admin/src/index.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;TodoPanel&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components/TodoPanel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-manager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;apis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEditViewSidePanel&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;TodoPanel&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;Once registered, the panel shows up automatically when editing entries.&lt;/p&gt;


  


&lt;h2&gt;
  
  
  &lt;strong&gt;Fetching and displaying tasks&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;At this point, the plugin has everything it needs on the backend:&lt;/p&gt;

&lt;p&gt;a content type, admin-only routes, and a custom endpoint to fetch tasks related to a specific entry.&lt;/p&gt;

&lt;p&gt;What’s missing is the admin-side logic that ties it all together.&lt;/p&gt;

&lt;p&gt;Inside the Content Manager edit view, we want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch the tasks related to the currently edited entry,&lt;/li&gt;
&lt;li&gt;display them inline in the sidebar,&lt;/li&gt;
&lt;li&gt;and update their state without reloading the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To handle data fetching, caching, and updates cleanly, we’ll use &lt;a href="https://tanstack.com/query" rel="noopener noreferrer"&gt;&lt;code&gt;@tanstack/react-query&lt;/code&gt;&lt;/a&gt;. It fits well here because it lets us express &lt;em&gt;“this list depends on the current document”&lt;/em&gt; and takes care of refetching when data changes.&lt;/p&gt;

&lt;p&gt;First, install it as a dependency of the plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @tanstack/react-query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Wiring React Query into the panel
&lt;/h3&gt;

&lt;p&gt;React Query works through a shared QueryClient. We'll create one and wrap the panel content in a QueryClientProvider by editing &lt;code&gt;./plugin-todo/admin/components/TodoPanel.tsx&lt;/code&gt;, then move the task-specific logic into a dedicated TaskList component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ./plugin-todo/admin/components/TodoPanel.tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unstable_useContentManagerContext&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;useContentManagerContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PanelComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/content-manager/strapi-admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;QueryClientProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;TaskList&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./TodoList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TodoPanel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PanelComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Todo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TaskList&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;QueryClientProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TodoPanel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fetching and updating tasks
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;TaskList&lt;/code&gt; component is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;determining &lt;em&gt;which&lt;/em&gt; entry is currently being edited,&lt;/li&gt;
&lt;li&gt;fetching its related tasks via the custom admin endpoint,&lt;/li&gt;
&lt;li&gt;rendering them as a checklist,&lt;/li&gt;
&lt;li&gt;and updating task state when a checkbox is toggled.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ./plugin-todo/admin/components/TodoList.tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unstable_useContentManagerContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useFetchClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/strapi/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQueryClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Checkbox&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/design-system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TaskList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;put&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFetchClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unstable_useContentManagerContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/todo/tasks/related/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateTaskMutation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;documentId&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="nl"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/todo/tasks/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&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="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleCheckboxChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;taskId&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="na"&gt;currentDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;updateTaskMutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;currentDone&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;documentId&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="nl"&gt;name&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="nl"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;marginTop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Checkbox&lt;/span&gt; 
            &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;onCheckedChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleCheckboxChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Checkbox&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TaskList&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this stage, the request to &lt;code&gt;/todo/tasks/related&lt;/code&gt; is working — but the list is empty. That’s expected: we haven’t created any tasks yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating tasks from the admin panel
&lt;/h3&gt;

&lt;p&gt;To add tasks directly from the Content Manager, we’ll use a modal built with the &lt;a href="https://design-system.strapi.io/" rel="noopener noreferrer"&gt;Strapi Design System&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The modal contains a small form that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;captures the task name,&lt;/li&gt;
&lt;li&gt;associates it with the currently edited document,&lt;/li&gt;
&lt;li&gt;creates the task through the plugin’s POST route,&lt;/li&gt;
&lt;li&gt;and refreshes the task list when the operation succeeds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create a new component at &lt;code&gt;admin/src/components/TodoModal.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ./plugin-todo/admin/components/TodoModal.tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/design-system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Field&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/design-system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Modal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/design-system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unstable_useContentManagerContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useFetchClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/strapi/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useQueryClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;onOpenChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TodoModal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onOpenChange&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;taskName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTaskName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unstable_useContentManagerContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFetchClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createTaskMutation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/todo/tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nf"&gt;setTaskName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;onOpenChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;createTaskMutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taskName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="na"&gt;related&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="na"&gt;__type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;id&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;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Root&lt;/span&gt; &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onOpenChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onOpenChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Create task&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Root&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"task"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Task&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt; 
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;taskName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ChangeEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTaskName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Root&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Footer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"tertiary"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Cancel&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; 
            &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;submitForm&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
            &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;createTaskMutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;taskName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;createTaskMutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            Confirm
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Footer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Root&lt;/span&gt;&lt;span class="p"&gt;&amp;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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TodoModal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail here is the &lt;code&gt;related&lt;/code&gt; field: we attach the task to the current document using its model type and ID, which makes the polymorphic relation work seamlessly across content types.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting everything in the panel
&lt;/h3&gt;

&lt;p&gt;Finally, we add a button to the sidebar panel that opens the modal and renders the task list below it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ./plugin-todo/admin/components/TodoList.tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unstable_useContentManagerContext&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;useContentManagerContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PanelComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/content-manager/strapi-admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TextButton&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/design-system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Plus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@strapi/icons&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;TaskList&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./TodoList&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;QueryClientProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;TodoModal&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./TodoModal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TodoPanel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PanelComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;modalOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setModalOpen&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useContentManagerContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Todo List&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TextButton&lt;/span&gt;
            &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setModalOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;startIcon&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Plus&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            Add todo
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;TextButton&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TodoModal&lt;/span&gt; &lt;span class="na"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;modalOpen&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onOpenChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setModalOpen&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TaskList&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;QueryClientProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TodoPanel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that in place, the plugin comes together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tasks appear next to content,&lt;/li&gt;
&lt;li&gt;tasks can be created and updated inline,&lt;/li&gt;
&lt;li&gt;and all the logic stays scoped to the plugin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Refreshing the admin panel now shows a fully functional todo list embedded directly in the Content Manager.&lt;/p&gt;


  


&lt;h2&gt;
  
  
  Deploying to production with Strapi Cloud
&lt;/h2&gt;

&lt;p&gt;So far, everything we’ve done has been running locally. The final step is making sure the plugin is built and available in production — and then actually seeing it live.&lt;/p&gt;

&lt;p&gt;Because plugin code is built separately from the main Strapi app, it needs to be compiled as part of the deployment process. A simple way to ensure this happens automatically is to add a &lt;code&gt;postinstall&lt;/code&gt; script to the root &lt;code&gt;package.json&lt;/code&gt; of your Strapi project:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"postinstall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"cd src/plugins/todo &amp;amp;&amp;amp; npm install &amp;amp;&amp;amp; npm run build"&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="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;This guarantees that whenever your project installs dependencies — locally, in CI, or in production — the plugin is installed and built as well.&lt;/p&gt;

&lt;p&gt;If you’re using &lt;strong&gt;Strapi Cloud&lt;/strong&gt;, you don’t need any additional setup.&lt;/p&gt;

&lt;p&gt;Once your project is connected to Strapi Cloud, every push triggers a deployment where dependencies are installed and build steps are executed. With the &lt;code&gt;postinstall&lt;/code&gt; script in place, your plugin is compiled automatically during that process.&lt;/p&gt;

&lt;p&gt;After the deployment finishes, open your Strapi Cloud project, navigate to the Content Manager, open any entry, and you should see your todo sidebar panel — just like in local development.&lt;/p&gt;

&lt;p&gt;At this point, the plugin is running in production, inside the same admin your editors use every day.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Repo
&lt;/h3&gt;

&lt;p&gt;The complete code can be found here: &lt;a href="https://github.com/strapi-community/plugin-todo" rel="noopener noreferrer"&gt;https://github.com/strapi-community/plugin-todo&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Final words
&lt;/h3&gt;

&lt;p&gt;This plugin is intentionally small, but it mirrors how real Strapi extensions are built and shipped.&lt;/p&gt;

&lt;p&gt;You defined a data model, scoped it to a plugin, exposed only the APIs you needed, and extended the admin interface where editors actually work. Nothing lives outside the system, and nothing special is required to deploy it.&lt;/p&gt;

&lt;p&gt;That’s the core idea behind Strapi plugins: they let you adapt the CMS to your product without turning customization into a separate project.&lt;/p&gt;

&lt;p&gt;If you want to explore further, the full plugin code is &lt;a href="https://github.com/strapi-community/plugin-todo" rel="noopener noreferrer"&gt;available on GitHub&lt;/a&gt;, and the Strapi community is always a good place to go deeper — whether you’re refining an internal workflow or building something meant to be reused by others.&lt;/p&gt;

</description>
      <category>strapi</category>
      <category>strapiplugins</category>
      <category>plugins</category>
    </item>
    <item>
      <title>Building Production-Ready Strapi + Next.js 16 Applications: Lessons from NOTUM Technologies</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 06 Feb 2026 12:56:51 +0000</pubDate>
      <link>https://dev.to/strapi/building-production-ready-strapi-nextjs-16-applications-lessons-from-notum-technologies-3idn</link>
      <guid>https://dev.to/strapi/building-production-ready-strapi-nextjs-16-applications-lessons-from-notum-technologies-3idn</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Who:&lt;/strong&gt; &lt;a href="https://www.notum.tech/" rel="noopener noreferrer"&gt;NOTUM Technologies&lt;/a&gt; is a digital agency based in Brno, Czech Republic, and has been a Strapi partner for 5+ years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; They created an open-source Next.js + &lt;a href="https://www.notumstrapi.com/" rel="noopener noreferrer"&gt;Strapi starter template&lt;/a&gt; that saves 3-4 weeks of development time per project.&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.amazonaws.com%2Fuploads%2Farticles%2Fxzk7j1in3iirhl696ap4.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.amazonaws.com%2Fuploads%2Farticles%2Fxzk7j1in3iirhl696ap4.png" alt="notum-starter.png" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Highlights:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monorepo architecture with full TypeScript type safety between Strapi and Next.js&lt;/li&gt;
&lt;li&gt;Supports projects from small startups to enterprise clients with billions of visitors&lt;/li&gt;
&lt;li&gt;Includes automatic page hierarchy with recursive full-path generation&lt;/li&gt;
&lt;li&gt;Built-in redirect management when page structures change&lt;/li&gt;
&lt;li&gt;Docker-ready deployment for any platform (Azure, AWS, Heroku, etc.)&lt;/li&gt;
&lt;li&gt;Uses PNPM for better dependency management and smaller build sizes&lt;/li&gt;
&lt;li&gt;Server-side populate logic to avoid frontend complexity and URL length limitations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Notable Plugins Created:&lt;/strong&gt; Location picker for maps, &lt;a href="https://market.strapi.io/plugins/@notum-cz-strapi-plugin-record-locking" rel="noopener noreferrer"&gt;Record locking&lt;/a&gt; for multi-user editing&lt;/p&gt;




&lt;p&gt;This article is based on the follwing discussion. Just in case you don't have the time to watch you can do a quick read instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction: From Pain Point to Production Solution
&lt;/h2&gt;

&lt;p&gt;Every agency knows the struggle: a new client comes in, and you're starting from scratch—again. Copy the old project, strip out the irrelevant components, implement new ones, and hope you haven't introduced any bugs along the way. For NOTUM Technologies, this cycle became unsustainable once they hit a critical mass of clients.&lt;/p&gt;

&lt;p&gt;The solution? A comprehensive starter template that distills years of production experience into a single, battle-tested package. But this isn't just another tutorial project—it's the foundation that has powered enterprise applications serving billions of visitors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Strapi? A Five-Year Partnership
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fndaxfwb7i4ttm78448iq.webp" 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.amazonaws.com%2Fuploads%2Farticles%2Fndaxfwb7i4ttm78448iq.webp" alt="68fa0769f5490b032ab21a01_hero-about-notum-p-800.webp" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NOTUM's journey with Strapi began around 2018-2019 when they were evaluating CMS options. After extensive internal discussions, Strapi emerged as the winner for several compelling reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flexibility at Scale:&lt;/strong&gt; Strapi works beautifully out of the box for simple websites, but it also allows deep customization through plugins and custom code. This dual nature makes it suitable for the wide variety of projects an agency encounters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugin Ecosystem:&lt;/strong&gt; NOTUM has contributed significantly to the Strapi ecosystem, creating plugins like the location picker (for map-based selection and proximity filtering) and record locking (preventing users from accidentally overwriting each other's changes).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community Building:&lt;/strong&gt; Beyond code contributions, NOTUM actively nurtures the local Strapi community through in-person meetups, including a V5 launch party. These gatherings provide valuable opportunities to share learnings and collect feedback directly from Strapi's team.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: Why a Monorepo?
&lt;/h2&gt;

&lt;p&gt;One of the most consequential decisions in the template is its monorepo structure, powered by Turborepo. This wasn't a trendy choice—it was driven by a real technical need.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Type Safety Problem
&lt;/h3&gt;

&lt;p&gt;Strapi doesn't automatically expose TypeScript types to frontend applications. In traditional setups, developers either manually maintain duplicate type definitions or accept the risk of runtime errors when schemas change. Neither option scales well.&lt;/p&gt;

&lt;p&gt;By using a monorepo, NOTUM solved this elegantly: auto-generated types from Strapi are exposed to the entire workspace. When you change a content schema, those changes automatically propagate to the frontend application. No more runtime surprises, no more manual syncing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;note&lt;/strong&gt;: the Strapi team actively is working on improving DX experience to allow users to expose Strapi types to their frontend project.  We will keep you posted with updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Package Structure
&lt;/h3&gt;

&lt;p&gt;The monorepo contains two main workspaces:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apps Workspace:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strapi&lt;/code&gt; - The backend CMS application&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ui&lt;/code&gt; - The Next.js frontend application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Packages Workspace:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shared ESLint and TypeScript configurations&lt;/li&gt;
&lt;li&gt;A shared data package for configuration variables (like supported locales)&lt;/li&gt;
&lt;li&gt;The Strapi types package (recently separated to reduce build dependencies)&lt;/li&gt;
&lt;li&gt;Design system with Tailwind configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This structure means you have a single source of truth for everything from linting rules to locale configurations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Next.js 16? The Jack of All Trades
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F87driw14hqx2lcorrayv.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.amazonaws.com%2Fuploads%2Farticles%2F87driw14hqx2lcorrayv.png" alt="next-16.png" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While the template specifically chose Next.js 16 for the frontend, the decision came from practical experience rather than hype.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versatility:&lt;/strong&gt; Agencies encounter everything from statically generated marketing sites to dynamic admin dashboards. Next.js handles all these use cases with the same framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team Adoption:&lt;/strong&gt; React's dominance in the job market means easier hiring and onboarding. The entire team could standardize on one framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Deployment:&lt;/strong&gt; Despite internet discourse about Next.js being tied to Vercel, NOTUM has successfully deployed to Azure, AWS S3, Heroku, and other enterprise hosting platforms without significant issues.&lt;/p&gt;

&lt;p&gt;The recent migration to Next.js 16 and React 19 did require switching from Yarn v1 to PNPM (due to multiple React version requirements with Strapi still on React 18), but this change actually improved build sizes and developer experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Editting Experience: Where V5 Shines
&lt;/h2&gt;

&lt;p&gt;The template's primary focus is creating an exceptional page editing experience, both for developers and content editors. &lt;/p&gt;

&lt;p&gt;Strapi V5's &lt;a href="https://docs.strapi.io/cms/features/preview" rel="noopener noreferrer"&gt;preview&lt;/a&gt; and &lt;a href="https://docs.strapi.io/cms/features/content-history" rel="noopener noreferrer"&gt;content history&lt;/a&gt; features were game-changers here, solving the long-standing headless CMS problem of not seeing what you're editing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic Page Hierarchy
&lt;/h3&gt;

&lt;p&gt;One standout feature is the automatic full-path generation system. Instead of manually managing URL paths, the template uses a parent-child relationship system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a root page (the homepage)&lt;/li&gt;
&lt;li&gt;Create child pages with just a slug and parent reference&lt;/li&gt;
&lt;li&gt;The system automatically computes full paths recursively&lt;/li&gt;
&lt;li&gt;Change any slug in the hierarchy, and all child paths update automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn't a plugin—it's built using Strapi's lifecycle hooks and the DB query builder. The implementation required some SQL magic since lifecycle hooks don't always provide references to current relation versions, but the result is seamless path management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic Redirects
&lt;/h3&gt;

&lt;p&gt;When page paths change, the system automatically generates redirect entries. No more broken links when restructuring your site's information architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Solving the Population Problem
&lt;/h2&gt;

&lt;p&gt;Anyone who's worked with Strapi at scale knows the populate challenge. With dynamic zones containing dozens (or hundreds) of components, the populate object becomes unwieldy.&lt;/p&gt;

&lt;h3&gt;
  
  
  The URL Length Limit
&lt;/h3&gt;

&lt;p&gt;Here's a problem most tutorials don't mention: when you have many components, your populate query parameters can exceed URL length limits. The frontend simply cannot request everything it needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-Side Population
&lt;/h3&gt;

&lt;p&gt;NOTUM's solution moves population logic to Strapi itself using document middleware:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The middleware detects specific request patterns (like fetching a single page)&lt;/li&gt;
&lt;li&gt;It checks for a custom &lt;code&gt;middleware_populate&lt;/code&gt; query parameter&lt;/li&gt;
&lt;li&gt;If conditions are met, it replaces the client's populate object with a pre-defined server-side version&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach keeps the frontend lean, avoids URL length issues, and provides a security benefit—you explicitly control what data can be populated.&lt;/p&gt;

&lt;p&gt;They originally used the popular "populate deep" plugin but abandoned it due to performance issues at scale.&lt;/p&gt;

&lt;p&gt;You can also accomplish this via route middleware to allow route based population, you can learn more about it &lt;a href="https://strapi.io/blog/route-based-middleware-to-handle-default-population-query-logic" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Developer Experience Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scripts for Common Tasks
&lt;/h3&gt;

&lt;p&gt;The template includes utility scripts for developer quality of life:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove all node_modules across the monorepo&lt;/li&gt;
&lt;li&gt;Clear build caches for all applications&lt;/li&gt;
&lt;li&gt;Quickly reset to a clean state when dealing with stale cache issues&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  GitHub Integration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Husky for commit message formatting (enforcing company-wide standards)&lt;/li&gt;
&lt;li&gt;GitHub Actions for build verification&lt;/li&gt;
&lt;li&gt;Dependabot configuration for automated dependency updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  VS Code Configuration
&lt;/h3&gt;

&lt;p&gt;Recommended extensions and workspace settings come pre-configured, though this area continues to evolve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Deployment
&lt;/h2&gt;

&lt;p&gt;The template ships with Docker support for maximum deployment flexibility. This wasn't added for theoretical completeness—it came from actual enterprise client requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tested Platforms:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure (used extensively on multiple projects)&lt;/li&gt;
&lt;li&gt;AWS S3 (for statically generated applications)&lt;/li&gt;
&lt;li&gt;Heroku (for teams preferring simpler deployment)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The containerized approach means you're not locked into any specific hosting provider, a crucial requirement for enterprise clients with existing infrastructure preferences.&lt;/p&gt;

&lt;p&gt;This a super power of Strapi, you can decide where and how you want to deploy it.  My favorite place is &lt;a href="https://strapi.io/cloud" rel="noopener noreferrer"&gt;Strapi Cloud&lt;/a&gt;. But you  can decide which approach works best for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned: Strapi V5 Lifecycle Gotchas
&lt;/h2&gt;

&lt;p&gt;NOTUM offered valuable feedback for developers diving deep into Strapi customization:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lifecycle Changes in V5:&lt;/strong&gt; The introduction of content history changed how lifecycles work. Publishing a new version now triggers the create lifecycle, which can cause unexpected behavior if you're not prepared for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relation Data Availability:&lt;/strong&gt; When making adjustments that don't change relations, those relations won't be present in the lifecycle payload. This is technically correct but not always obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin Panel vs API Payloads:&lt;/strong&gt; Calling the same endpoint from the admin panel versus a custom API client produces different lifecycle payloads. This inconsistency can trip up even experienced developers.&lt;/p&gt;

&lt;p&gt;The takeaway isn't that these behaviors are wrong—they often have valid technical reasons—but they're not always well documented. NOTUM recommends approaching lifecycles with caution and always checking actual behavior against assumptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;note:&lt;/strong&gt; Did you know, you can alway provide feedback and feature request &lt;a href="https://feedback.strapi.io/" rel="noopener noreferrer"&gt;here&lt;/a&gt; or continue the conversation in &lt;a href="https://github.com/strapi/strapi/discussions" rel="noopener noreferrer"&gt;GitHub discussions&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI-Assisted Development: The Evolving Workflow
&lt;/h2&gt;

&lt;p&gt;NOTUM is actively exploring AI integration in their development workflow, particularly for documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building an internal knowledge base in Markdown&lt;/li&gt;
&lt;li&gt;Experimenting with AI agents that understand the template's specifics&lt;/li&gt;
&lt;li&gt;Working toward publishable "skills" for AI tools that understand their codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Their philosophy aligns with broader industry observations: there's no one-size-fits-all AI workflow. Each developer should find what makes them productive rather than copying someone else's setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;The template is designed for quick setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href=""&gt;Clone the repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Ensure you're using the correct Node version (the repo includes version configuration)&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;pnpm install&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Start Docker (required for the database)&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;pnpm run dev:strapi&lt;/code&gt; to start the backend&lt;/li&gt;
&lt;li&gt;Create an API token in Strapi's admin panel&lt;/li&gt;
&lt;li&gt;Add the token to your Next.js &lt;code&gt;.env.local&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Run the frontend&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check out this video on how to set the project up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: Why Templates Matter
&lt;/h2&gt;

&lt;p&gt;The difference between a tutorial template and a production template is measured in months of real-world usage. NOTUM's starter represents the accumulated wisdom from dozens of client projects, from startup landing pages to enterprise applications.&lt;/p&gt;

&lt;p&gt;Every time they complete a project—or even mid-project when something important emerges—they backport those learnings into the template. This continuous improvement cycle means you're not just getting a starting point; you're getting a documented history of what works (and what doesn't) in production.&lt;/p&gt;

&lt;p&gt;Whether you use the template directly or study it as a reference for your own architecture, there's value in learning from a team that has shipped—and maintained—this stack at scale.&lt;/p&gt;

&lt;p&gt;You can also checkout out our official Strapi Stater project called &lt;a href="https://strapi.io/blog/getting-started-with-strapi-and-next-js-launchpad-the-official-strapi-demo" rel="noopener noreferrer"&gt;Launchpad&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NOTUM Technologies:&lt;/strong&gt; For companies needing development partners with deep Strapi expertise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository:&lt;/strong&gt; Open-source starter template (link in video description)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strapi Community:&lt;/strong&gt; Join local meetups to connect with other developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Did you know?&lt;/strong&gt;&lt;br&gt;
Strapi hosts Open Office Hours on Discord Monday through Friday at 12:30 PM CST (running until about 1:30 PM CST). It's a great way to connect, get help, and share your feedback directly with the team.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was created based on a conversation with Dominic from NOTUM Technologies about their Next.js + Strapi starter template.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://strapi.io/blog/building-production-ready-strapi-next-js-16-applications-lessons-from-notum-technologies" rel="noopener noreferrer"&gt;strapi.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>strapi</category>
    </item>
    <item>
      <title>Building Native Apps With Expo and React Native</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 06 Feb 2026 12:52:43 +0000</pubDate>
      <link>https://dev.to/javascriptar/building-native-apps-with-expo-and-react-native-2ine</link>
      <guid>https://dev.to/javascriptar/building-native-apps-with-expo-and-react-native-2ine</guid>
      <description>&lt;p&gt;&lt;strong&gt;This post is based on our livestream conversation with &lt;a href="https://twitter.com/kadikraman" rel="noopener noreferrer"&gt;Kadi Kraman&lt;/a&gt;, Developer Experience Engineer at &lt;a href="https://docs.expo.dev/" rel="noopener noreferrer"&gt;Expo&lt;/a&gt;.&lt;/strong&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expo is the Next.js of React Native&lt;/strong&gt; — it handles all the complex native tooling so you can focus on building your app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with Expo Go&lt;/strong&gt; for learning and prototyping; switch to &lt;strong&gt;development builds&lt;/strong&gt; for production (no limitations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CNG (Continuous Native Generation)&lt;/strong&gt; means you never commit native folders — they're auto-generated and disposable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Native renders actual native components&lt;/strong&gt;, unlike Flutter which draws its own UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three styling options&lt;/strong&gt;: Built-in StyleSheet, NativeWind (Tailwind), or Unistyles (best for theming)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS services&lt;/strong&gt; handle building, submitting, and updating your apps — no Mac required for iOS builds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can write server code&lt;/strong&gt; directly in your React Native app with &lt;code&gt;+api.ts&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta officially recommends Expo&lt;/strong&gt; for React Native development&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Fyp3xjrtra1p17erakbbo.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.amazonaws.com%2Fuploads%2Farticles%2Fyp3xjrtra1p17erakbbo.png" alt="002-expo.png" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Choose Expo for Mobile Development?
&lt;/h2&gt;

&lt;p&gt;Think of Expo as the Next.js of React Native. Just as you wouldn't manually configure Webpack and bundlers when building a React web app, Expo eliminates the complexity of native mobile development tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem Expo Solves&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reactnative.dev" rel="noopener noreferrer"&gt;React Native&lt;/a&gt; by itself is a toolkit of APIs and a JavaScript runtime, but it doesn't provide a complete framework for building apps. For example, navigation between screens isn't included out of the box. &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.amazonaws.com%2Fuploads%2Farticles%2Flkhop8zwun31ficu5bcf.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.amazonaws.com%2Fuploads%2Farticles%2Flkhop8zwun31ficu5bcf.png" alt="react-native.png" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without Expo, you'd need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manually configure native build tools (Xcode, Android Studio)&lt;/li&gt;
&lt;li&gt;Research and choose from dozens of libraries for basic functionality&lt;/li&gt;
&lt;li&gt;Manage native iOS and Android folders manually&lt;/li&gt;
&lt;li&gt;Handle complex upgrade paths when React Native versions change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What Expo Provides&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Expo is a collection of libraries, utilities, bundlers, and a complete ecosystem that makes building React Native apps pleasant and straightforward. Key benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expo SDK&lt;/strong&gt;: A curated set of native libraries guaranteed to work together (video, audio, haptics, push notifications, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Router&lt;/strong&gt;: File-based routing similar to Next.js&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Go&lt;/strong&gt;: Test your app instantly on your phone without building native binaries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CNG (Continuous Native Generation)&lt;/strong&gt;: Native folders are generated automatically and can be thrown away and regenerated&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  React Native vs. Flutter: Understanding the Difference
&lt;/h2&gt;

&lt;p&gt;A common question is how React Native compares to Flutter. The key distinction is in rendering: Flutter renders on Skia (a graphics engine), essentially drawing its own UI that hooks into native APIs. React Native, on the other hand, renders actual native components. When you build a button in React Native, it's a real native button, not a simulation. This is why React Native apps are often described as "native apps" — because they truly are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started: Expo Go vs. Development Builds
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F463ec3kgawetkqv87stm.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.amazonaws.com%2Fuploads%2Farticles%2F463ec3kgawetkqv87stm.png" alt="expo-go.png" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Expo offers two approaches for development, each suited to different stages of your project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expo Go: Perfect for Learning and Prototyping&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Expo Go is an app you download from the App Store or Play Store. It contains pre-built native code for the most common libraries, allowing you to start developing immediately without any native build tools. Simply write JavaScript/TypeScript, and your code runs instantly on your device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Learning React Native, rapid prototyping, hackathons, and getting that first "app on your phone" moment. This instant feedback loop is why many AI-powered app builders (like Replit) use Expo under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations:&lt;/strong&gt; Not all libraries are included. Some Expo SDK libraries (like expo-video) and third-party packages won't work in Expo Go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Development Builds: Production-Ready Flexibility&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you're ready for production or need libraries not included in Expo Go, you switch to development builds. Instead of using the Expo Go app, you create your own native shell with exactly the libraries you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Production apps, custom native code, any library from the React Native ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; There are no limitations with development builds. Once you move past Expo Go, you have full access to native code. You can even write custom native modules in Swift or Kotlin using the Expo Modules API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous Native Generation (CNG): The Secret Sauce
&lt;/h2&gt;

&lt;p&gt;One of Expo's most powerful features is CNG (also called "prebuild"). Traditional React Native development requires you to commit and maintain iOS and Android folders, managing native code directly. With CNG:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your native folders are git-ignored&lt;/li&gt;
&lt;li&gt;They're generated automatically from your package.json, app.json, and node_modules&lt;/li&gt;
&lt;li&gt;Upgrades become trivial: delete the native folders, regenerate with new versions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you need to modify native configuration (like editing Info.plist or the app delegate), you create &lt;strong&gt;config plugins&lt;/strong&gt; instead of editing files directly. These plugins run during generation, making your changes repeatable and testable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling Your App: Three Approaches
&lt;/h2&gt;

&lt;p&gt;Coming from web development, you might wonder about styling options. React Native offers several approaches:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;StyleSheet (Built-in)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React Native's built-in styling API. Can be verbose for theming.&lt;/td&gt;
&lt;td&gt;Projects that want zero dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NativeWind&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tailwind CSS for React Native. Familiar utility classes.&lt;/td&gt;
&lt;td&gt;Web developers wanting quick start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unistyles&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Like StyleSheet but with better theming support (light/dark mode).&lt;/td&gt;
&lt;td&gt;Apps needing robust theming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; If you want your app to look distinctly "native" rather than like a website, study the Apple Human Interface Guidelines and Google Material Design specs. Expo UI (currently in beta) is also exposing more native components like segmented controls and glass buttons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment: From Code to App Store
&lt;/h2&gt;

&lt;p&gt;Deploying mobile apps is fundamentally different from web deployment. You need to handle code signing (certificates and provisioning profiles) and pass through a manual review process. Expo offers several services to streamline this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EAS Build&lt;/strong&gt;: Cloud-based CI/CD that builds your native apps. Particularly valuable because you need a Mac to build iOS apps — EAS Build provides that infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS Submit&lt;/strong&gt;: Uploads your built apps to the App Store and Play Store. Free to use and essential since Apple requires specific tools (Transporter) to upload apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS Update&lt;/strong&gt;: Push JavaScript updates without going through app store review. Fix bugs instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://launch.expo.dev/" rel="noopener noreferrer"&gt;Expo Launch&lt;/a&gt;&lt;/strong&gt;: A new beta product that takes a GitHub repo and handles everything — building, signing, and submission. Currently supports public repos and iOS (Play Store coming soon).&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Fpd0c9zzr192sexyz3na6.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.amazonaws.com%2Fuploads%2Farticles%2Fpd0c9zzr192sexyz3na6.png" alt="Screenshot 2026-01-22 at 12.30.28 PM.png" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Server Code in Your React Native App
&lt;/h2&gt;

&lt;p&gt;A powerful recent addition is the ability to write API routes directly in your React Native codebase. Create files ending in &lt;code&gt;+api.ts&lt;/code&gt; in your source folder:&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="c1"&gt;// src/posts+api.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from server!&lt;/span&gt;&lt;span class="dl"&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;Environment variables without the &lt;code&gt;EXPO_PUBLIC_&lt;/code&gt; prefix are only accessible in these server routes, keeping your secrets safe. Deploy with EAS Hosting and you have a full-stack mobile app with backend API routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-Assisted Development with Expo
&lt;/h2&gt;

&lt;p&gt;The Expo ecosystem is particularly well-suited for AI-assisted development. With Expo Go's instant feedback loop, tools like Replit and various AI coding assistants can help users go from idea to working app in minutes.&lt;/p&gt;

&lt;p&gt;However, there's an important distinction between "vibe coding" (letting AI write everything) and AI-assisted development. Even when using AI tools, you still own the code and are responsible for testing, iterating, and ensuring quality. AI can give you a starting point, but professional development still requires understanding what the code does.&lt;/p&gt;

&lt;p&gt;Resources like MCPs (Model Context Protocols), cursor rules, and Claude rules allow experts to codify best practices that AI tools can use as a source of truth — resulting in much higher quality generated code than generic training data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming Soon: Performance Observability
&lt;/h2&gt;

&lt;p&gt;Expo is developing an observability product for monitoring app performance — think Lighthouse scores but for native apps. You'll be able to track metrics like JavaScript bundle load time, see how different app versions affect performance, and identify slow devices or sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start Checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install Node.js&lt;/strong&gt; and create a new project with &lt;code&gt;npx create-expo-app@latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download Expo Go&lt;/strong&gt; on your phone for instant testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;npx expo start&lt;/code&gt;&lt;/strong&gt; and scan the QR code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the Expo docs&lt;/strong&gt; (docs.expo.dev) — they have an AI assistant built-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check React Native Directory&lt;/strong&gt; (reactnative.directory) for community libraries&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Additional Learning Resources
&lt;/h2&gt;

&lt;p&gt;To continue your learning journey, here are some excellent resources:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full Stack React Native Course by Tube Guruji&lt;/strong&gt;: A comprehensive 4+ hour course building a local business directory app from scratch using React Native, Expo, and a full backend. Perfect for hands-on learning. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Written Tutorial&lt;/strong&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Folsia0b9o07f0wnqks7l.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.amazonaws.com%2Fuploads%2Farticles%2Folsia0b9o07f0wnqks7l.png" alt="Your paragraph text.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="(https://strapi.io/blog/building-a-react-native-app-with-expo-and-strapi-a-complete-guide)"&gt;Building a React Native App with Expo and Strapi: A Complete Guide&lt;/a&gt;&lt;/strong&gt;: A detailed step-by-step tutorial from the Strapi blog covering project setup, NativeWind styling, data fetching with React Query, and building a complete blog app with infinite scroll. &lt;a href="(https://strapi.io/blog/building-a-react-native-app-with-expo-and-strapi-a-complete-guide)"&gt;Read it here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Expo has transformed React Native development from a complex, native-tooling-heavy process into something that feels almost as simple as web development. For React developers, there's never been a better time to start building mobile apps.&lt;/p&gt;

&lt;p&gt;The key takeaways: Start with Expo Go for learning, graduate to development builds for production, embrace CNG to avoid native folder maintenance, and leverage EAS services for painless deployment. With these tools, you can go from idea to app store in record time.&lt;/p&gt;

&lt;p&gt;Meta officially recommends Expo for React Native development, and it's clear why: the developer experience is exceptional, the ecosystem is mature, and the path from prototype to production is smoother than ever.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Happy building!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Originally published at &lt;a href="https://strapi.io/blog/building-native-apps-with-expo-and-react-native" rel="noopener noreferrer"&gt;strapi.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Bye 2025, Hello 2026! A year in review</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 06 Feb 2026 12:49:08 +0000</pubDate>
      <link>https://dev.to/strapi/bye-2025-hello-2026-a-year-in-review-1a89</link>
      <guid>https://dev.to/strapi/bye-2025-hello-2026-a-year-in-review-1a89</guid>
      <description>&lt;p&gt;We’ve just wrapped up 2025 - and what a year it has been! &lt;br&gt;
As we do every single year, let’s take a moment to celebrate all the achievements of the past 12 months, and share what’s coming next!&lt;/p&gt;

&lt;p&gt;Before we go through the details, to all of you who have used Strapi: thank you! By being part of the community, you have contributed to elevating the Strapi open-source project to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;70K+ stars on GitHub (&lt;a href="https://github.com/strapi/strapi/stargazers" rel="noopener noreferrer"&gt;add yours!&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;36K commits&lt;/li&gt;
&lt;li&gt;9.4K pull requests merged&lt;/li&gt;
&lt;li&gt;Thousands of customers, including Adidas, Airbus, Amazon, Cisco, Tesco, Toyota, PostHog, and CodeRabbit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These milestones are especially meaningful, as &lt;a href="https://strapi.io/blog/str-happy-10th-birthday-strapi" rel="noopener noreferrer"&gt;Strapi turned 10&lt;/a&gt; this year!&lt;/p&gt;

&lt;p&gt;12 months ago, &lt;a href="https://strapi.io/blog/bye-2024-hello-2025-a-year-in-review" rel="noopener noreferrer"&gt;we shared&lt;/a&gt; that we would focus on the Side Editor, the New Cloud Plan, the JavaScript/TypeScript SDK, the AI tools, and the first in-person StrapiConf. And that is exactly what we delivered!&lt;/p&gt;

&lt;h2&gt;
  
  
  Product improvements
&lt;/h2&gt;

&lt;p&gt;More than ever before, the Strapi community shipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strapi AI
&lt;/h3&gt;

&lt;p&gt;AI is changing everyone’s job. You have to disrupt yourself, and so do we. Content Management Systems sit at the intersection of Developers and Marketers, who both have to reinvent their jobs with code generation (vibe coding and AI coding) and content creation (gen AI). The way &lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7406368766550237184/" rel="noopener noreferrer"&gt;CMSs are used continues to evolve at an incredible pace&lt;/a&gt;. What an exciting time to be building!&lt;/p&gt;

&lt;p&gt;This is why we invested heavily in AI. In 2025, we unveiled a suite of strategic product enhancements designed to accelerate both developer workflows and content editor experiences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Content-Type Builder&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It all started with the AI Content-Type Builder that we announced at StrapiConf (see below). With the brand new AI Content-Type Builder, you can generate content types and components from natural language prompts or uploaded frontend apps/Figma designs. This eliminates redundant tasks, saving days of development time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Media Library&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Creating alt text and captions is another repetitive -and often overlooked- task. The AI Media Library handles this automatically whenever you upload an asset.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;&lt;strong&gt;AI Translations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you manage a website in multiple languages, translating content can be a lengthy, costly, and error-prone process. The AI Translations feature has you covered. When enabled, it automatically translates your content into all the locales you selected. This marks an important step in Strapi’s evolution toward an agentic CMS. Merci beaucoup!&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;These are the first steps into the Strapi AI journey. Stay tuned for more!&lt;/p&gt;

&lt;h3&gt;
  
  
  Editing Experience
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Live Preview&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Headless architecture is a great separation of concerns between the frontend and the CMS. However, this separation often came at the cost of the editing experience. Non-technical people need - and deserve - to edit websites and apps in an intuitive and smooth manner. Combined with Strapi 5's Draft &amp;amp; Publish capabilities, Live Preview addresses this challenge.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;p&gt;&lt;strong&gt;Responsive&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Editing content for small screens has been a community request for several years. We must admit this comes later than expected, but Strapi is becoming responsive. We've released the first improvements and will keep rolling out additional updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conditional Fields
&lt;/h3&gt;

&lt;p&gt;At Strapi, we’ve always prioritized giving developers and content teams the flexibility to organize content as they see fit. So we introduced &lt;strong&gt;Conditional Fields&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With Conditional Fields, you can dynamically show or hide fields based on specific criteria like dropdown selections or checkbox values. This helps editors focus on the right fields at the right time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer Experience
&lt;/h2&gt;

&lt;p&gt;Strapi was created by developers, for developers. In 2025, we shipped a significant number of Developer Experience improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strapi Client Library&lt;/strong&gt;: The brand new SDK allows you to easily request your Strapi content from your favorite frontend framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAPI support&lt;/strong&gt;: We’ve heard many users requesting the ability to benefit from typing when using the Strapi Client Library in a frontend project (Next.js, Tanstack, Astro, Nuxt.js, etc.). OpenAPI support will allow us to add this to the Strapi Client Library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Homepage widgets&lt;/strong&gt;: From now on, it is possible to display and create widgets on the Strapi homepage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP prototype&lt;/strong&gt;: The first MCP prototype was shipped. This will be a massive enabler in making Strapi fully agentic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Integrations
&lt;/h2&gt;

&lt;p&gt;A Headless CMS is often used in conjunction with other services, making it the foundation of a composable architecture. In 2025,  in close collaboration with our partner &lt;a href="https://strapi.io/partners/VirtusLab" rel="noopener noreferrer"&gt;VirtusLab&lt;/a&gt;, we launched official &lt;a href="https://strapi.io/blog/official-integrations-and-homepage-widget-api" rel="noopener noreferrer"&gt;integrations for Shopify, BigCommerce, and Cloudinary&lt;/a&gt;.&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.amazonaws.com%2Fuploads%2Farticles%2Fft2chia4vik5dbk0dr8e.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.amazonaws.com%2Fuploads%2Farticles%2Fft2chia4vik5dbk0dr8e.png" alt="strapi official integrations.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Strapi Cloud
&lt;/h3&gt;

&lt;p&gt;24 months after its launch, Strapi Cloud has become the leading platform for hosting Strapi projects, as it comes with everything you need: Node.js hosting, a Postgres database, asset storage, a CDN, GitHub/GitLab integrations, and more.&lt;/p&gt;

&lt;p&gt;Until 2025, the most affordable plan was the Pro plan, at $99/month. Many of you have shared that this price was too high for small projects. This is why we introduced the &lt;strong&gt;Essential Plan&lt;/strong&gt;, for $15/month (when billed annually). We even went a step further by launching a Free Plan, making Strapi Cloud accessible to everyone.&lt;/p&gt;

&lt;p&gt;Based on your feedback, we released a bunch of other improvements, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ownership transfer&lt;/strong&gt;: Give full access to a Strapi Cloud to anyone (ideally for agencies with clients, and vice versa).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data migration&lt;/strong&gt;: Easily migrate content from one environment to another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downloadable backups&lt;/strong&gt;: Save your content and store it anywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Community milestones
&lt;/h2&gt;

&lt;p&gt;Strapi is so much more than an open-source project; it is a vibrant community of contributors, users, and partners who build faster, together.&lt;/p&gt;

&lt;h3&gt;
  
  
  StrapiConf
&lt;/h3&gt;

&lt;p&gt;After organizing 3 online StrapiConf, we organized the first in-person StrapiConf on May 13, 2025. After years of chats on the &lt;a href="http://discord.strapi.io/" rel="noopener noreferrer"&gt;Strapi Community Discord&lt;/a&gt;, it was fantastic to meet so many of you face-to-face!&lt;/p&gt;

&lt;p&gt;Community members presented incredible plugins. Large organizations, including &lt;a href="https://www.youtube.com/watch?v=pjJNnW0UmgA&amp;amp;pp=2AYO" rel="noopener noreferrer"&gt;Adidas&lt;/a&gt; and &lt;a href="https://www.youtube.com/watch?v=NkkM2fNKCV8" rel="noopener noreferrer"&gt;Airbus&lt;/a&gt;, benefit from the unique customization capabilities that Strapi offers. We had a blast!&lt;/p&gt;

&lt;p&gt;Thank you again to our &lt;a href="https://conf.strapi.io/#sponsors" rel="noopener noreferrer"&gt;amazing sponsors&lt;/a&gt; for empowering the Strapi ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Community Calls and Events
&lt;/h3&gt;

&lt;p&gt;We had some amazing calls with the community, one of which was a full-on conversation with &lt;a href="https://www.linkedin.com/in/kentcdodds" rel="noopener noreferrer"&gt;Kent C. Dodds&lt;/a&gt; about the future of the web, AI agents, and why Model Context Protocol (MCP) might be as big a shift as the early days of the internet.&lt;/p&gt;

&lt;p&gt;Here are some other talks and events you may have missed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://youtu.be/k4oUvMWpzBk" rel="noopener noreferrer"&gt;Strapi Community Call: Strapi AI, Media Library AI, Plugin Generators (with Boaz) &amp;amp;  Q&amp;amp;A&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtube.com/live/TbqChjpsTpo" rel="noopener noreferrer"&gt;Community Call January 2025: What's New and AMA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/gcDVX_pyrYc" rel="noopener noreferrer"&gt;Strapi July Community Call 2025&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To join live streams &amp;amp; online meetings on the latest updates &amp;amp; discussions about Strapi, visit our events page: &lt;a href="https://strapi.io/events" rel="noopener noreferrer"&gt;https://strapi.io/events&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tutorials
&lt;/h3&gt;

&lt;p&gt;Among many amazing tutorials, we created a full tutorial on “&lt;a href="https://www.youtube.com/watch?v=Q-cPtlYG1cY" rel="noopener noreferrer"&gt;Strapi 5 and Next.js 15 Full Stack Project Course&lt;/a&gt;”, which shows you how to build a fully functional summer camp website from start to finish, through hands-on practice.&lt;/p&gt;

&lt;p&gt;Other great tutorials from the community that you may want to see include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://youtu.be/t1iUuap7vhw?si=aMslVieJJXaG3za1" rel="noopener noreferrer"&gt;Strapi 5 Crash Course 2025 Full Tutorial For Beginners&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/pW2Pso4fw5g?si=6kcDogq2IRs0EWRP" rel="noopener noreferrer"&gt;&lt;strong&gt;React Native Project | Build Full Stack React Native App with Expo, &amp;amp; Strapi | AI Recipe Generator&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/fO3D8lNs10c?si=0o_Vlq2xdieEqM0-" rel="noopener noreferrer"&gt;Build Your Own MASTERCLASS clone in React Native&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=VosHdPQr6nI" rel="noopener noreferrer"&gt;Next.js 16 + Strapi 5: Caché components, autenticación y despliegue gratis a producción&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To see more tutorials, visit the &lt;a href="https://strapi.io/blog" rel="noopener noreferrer"&gt;Strapi Blog&lt;/a&gt; or our &lt;a href="https://www.youtube.com/strapi" rel="noopener noreferrer"&gt;YouTube channel&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Marketplace
&lt;/h3&gt;

&lt;p&gt;We introduced several improvements to the Strapi Marketplace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Plugins &amp;amp; Providers are now merged into one overview&lt;/li&gt;
&lt;li&gt;The overview is sorted by the number of NPM downloads by default&lt;/li&gt;
&lt;li&gt;The home page now directly shows the overview&lt;/li&gt;
&lt;li&gt;Outdated plugins for Strapi v4 have been removed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you to the top plugin creators: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mateusz Ziarko - VirtusLab&lt;/li&gt;
&lt;li&gt;Scherwan Al-Ahmad - MagicDX&lt;/li&gt;
&lt;li&gt;Kevin Vugts - Headlockr&lt;/li&gt;
&lt;li&gt;Tim Schipper - PluginPal&lt;/li&gt;
&lt;li&gt;Nikita Shenderov - CKEditor plugin&lt;/li&gt;
&lt;li&gt;Laurent Cazanove - Meilisearch&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Partner Program
&lt;/h3&gt;

&lt;p&gt;The Partner Program continues to become increasingly beneficial for both enterprises seeking trusted implementers and agencies expanding their client base. &lt;/p&gt;

&lt;p&gt;Special shout-out to Notum, QBurst, Alloy, TenTwenty, and Thrillworks for becoming Enterprise Partners, totaling 5 Enterprise-level partners added in 1 year.&lt;/p&gt;

&lt;p&gt;This year, we launched monthly live Partner Enablement sessions to keep our partners informed on Strapi best practices, product updates, and sales alignment.&lt;/p&gt;

&lt;p&gt;Partners have consistently shared valuable feedback with our product and engineering teams. They've helped identify bugs, feature gaps, and opportunities to price the product more competitively. Thank you for that!&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking ahead to 2026
&lt;/h2&gt;

&lt;p&gt;As you have seen in this article, we have shipped numerous new features, just as we have for the past few years. This is great!&lt;/p&gt;

&lt;p&gt;Over the past few months, you all have shared your opinions through the Community Survey and the &lt;a href="https://github.com/strapi/strapi/issues/24956" rel="noopener noreferrer"&gt;pain point GitHub issue&lt;/a&gt;. A few improvement requests have been around for a long time on the &lt;a href="https://feedback.strapi.io/" rel="noopener noreferrer"&gt;Feedback page&lt;/a&gt;. This is largely because we needed to build a solid company to make the open-source project sustainable and navigate through &lt;a href="https://www.youtube.com/watch?v=AfVzyKl_-kw" rel="noopener noreferrer"&gt;2 different business models&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We’ve heard you loud and clear. Although adding features is exciting, it is time to improve some of the foundations. That's why 2026 will be dedicated to User Experience. And it all starts with Quality - fixing bugs, improving data migration, and strengthening the foundations - while also encompassing both the Developer Experience (MCP, typing in the frontend) and the Editing Experience (Media Library enhancements, etc.). Regarding Strapi Cloud, we are migrating to a new infrastructure that will significantly enhance performance, but also allow us to provide you with direct database access and improved runtime logs.&lt;/p&gt;

&lt;p&gt;We also plan to continue collaborating as a Strapi community, including creating plugins, contributing to the core, and hosting events.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A Heartfelt Thank You&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Thank you so much for being part of the Strapi community. We are 10 years into this journey, and it feels like we are just getting started.&lt;/p&gt;

&lt;p&gt;Str’happy New Year!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://strapi.io/blog/bye-2025-hello-2026-a-year-in-review" rel="noopener noreferrer"&gt;strapi.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>community</category>
      <category>strapi</category>
    </item>
    <item>
      <title>Building a React Native App with Expo and Strapi: A Complete Guide</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 06 Feb 2026 12:45:51 +0000</pubDate>
      <link>https://dev.to/strapi/building-a-react-native-app-with-expo-and-strapi-a-complete-guide-5fma</link>
      <guid>https://dev.to/strapi/building-a-react-native-app-with-expo-and-strapi-a-complete-guide-5fma</guid>
      <description>&lt;p&gt;&lt;strong&gt;This post is based on our livestream conversation with &lt;a href="https://twitter.com/kadikraman" rel="noopener noreferrer"&gt;Kadi Kraman&lt;/a&gt;, Developer Experience Engineer at &lt;a href="https://docs.expo.dev/" rel="noopener noreferrer"&gt;Expo&lt;/a&gt;.&lt;/strong&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expo is the Next.js of React Native&lt;/strong&gt; — it handles all the complex native tooling so you can focus on building your app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with Expo Go&lt;/strong&gt; for learning and prototyping; switch to &lt;strong&gt;development builds&lt;/strong&gt; for production (no limitations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CNG (Continuous Native Generation)&lt;/strong&gt; means you never commit native folders — they're auto-generated and disposable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Native renders actual native components&lt;/strong&gt;, unlike Flutter which draws its own UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three styling options&lt;/strong&gt;: Built-in StyleSheet, NativeWind (Tailwind), or Unistyles (best for theming)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS services&lt;/strong&gt; handle building, submitting, and updating your apps — no Mac required for iOS builds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can write server code&lt;/strong&gt; directly in your React Native app with &lt;code&gt;+api.ts&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta officially recommends Expo&lt;/strong&gt; for React Native development&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Fyp3xjrtra1p17erakbbo.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.amazonaws.com%2Fuploads%2Farticles%2Fyp3xjrtra1p17erakbbo.png" alt="002-expo.png" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Choose Expo for Mobile Development?
&lt;/h2&gt;

&lt;p&gt;Think of Expo as the Next.js of React Native. Just as you wouldn't manually configure Webpack and bundlers when building a React web app, Expo eliminates the complexity of native mobile development tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem Expo Solves&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reactnative.dev" rel="noopener noreferrer"&gt;React Native&lt;/a&gt; by itself is a toolkit of APIs and a JavaScript runtime, but it doesn't provide a complete framework for building apps. For example, navigation between screens isn't included out of the box. &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.amazonaws.com%2Fuploads%2Farticles%2Flkhop8zwun31ficu5bcf.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.amazonaws.com%2Fuploads%2Farticles%2Flkhop8zwun31ficu5bcf.png" alt="react-native.png" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without Expo, you'd need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manually configure native build tools (Xcode, Android Studio)&lt;/li&gt;
&lt;li&gt;Research and choose from dozens of libraries for basic functionality&lt;/li&gt;
&lt;li&gt;Manage native iOS and Android folders manually&lt;/li&gt;
&lt;li&gt;Handle complex upgrade paths when React Native versions change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What Expo Provides&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Expo is a collection of libraries, utilities, bundlers, and a complete ecosystem that makes building React Native apps pleasant and straightforward. Key benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expo SDK&lt;/strong&gt;: A curated set of native libraries guaranteed to work together (video, audio, haptics, push notifications, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Router&lt;/strong&gt;: File-based routing similar to Next.js&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Go&lt;/strong&gt;: Test your app instantly on your phone without building native binaries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CNG (Continuous Native Generation)&lt;/strong&gt;: Native folders are generated automatically and can be thrown away and regenerated&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  React Native vs. Flutter: Understanding the Difference
&lt;/h2&gt;

&lt;p&gt;A common question is how React Native compares to Flutter. The key distinction is in rendering: Flutter renders on Skia (a graphics engine), essentially drawing its own UI that hooks into native APIs. React Native, on the other hand, renders actual native components. When you build a button in React Native, it's a real native button, not a simulation. This is why React Native apps are often described as "native apps" — because they truly are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started: Expo Go vs. Development Builds
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F463ec3kgawetkqv87stm.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.amazonaws.com%2Fuploads%2Farticles%2F463ec3kgawetkqv87stm.png" alt="expo-go.png" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Expo offers two approaches for development, each suited to different stages of your project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expo Go: Perfect for Learning and Prototyping&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Expo Go is an app you download from the App Store or Play Store. It contains pre-built native code for the most common libraries, allowing you to start developing immediately without any native build tools. Simply write JavaScript/TypeScript, and your code runs instantly on your device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Learning React Native, rapid prototyping, hackathons, and getting that first "app on your phone" moment. This instant feedback loop is why many AI-powered app builders (like Replit) use Expo under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations:&lt;/strong&gt; Not all libraries are included. Some Expo SDK libraries (like expo-video) and third-party packages won't work in Expo Go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Development Builds: Production-Ready Flexibility&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you're ready for production or need libraries not included in Expo Go, you switch to development builds. Instead of using the Expo Go app, you create your own native shell with exactly the libraries you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Production apps, custom native code, any library from the React Native ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; There are no limitations with development builds. Once you move past Expo Go, you have full access to native code. You can even write custom native modules in Swift or Kotlin using the Expo Modules API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous Native Generation (CNG): The Secret Sauce
&lt;/h2&gt;

&lt;p&gt;One of Expo's most powerful features is CNG (also called "prebuild"). Traditional React Native development requires you to commit and maintain iOS and Android folders, managing native code directly. With CNG:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your native folders are git-ignored&lt;/li&gt;
&lt;li&gt;They're generated automatically from your package.json, app.json, and node_modules&lt;/li&gt;
&lt;li&gt;Upgrades become trivial: delete the native folders, regenerate with new versions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you need to modify native configuration (like editing Info.plist or the app delegate), you create &lt;strong&gt;config plugins&lt;/strong&gt; instead of editing files directly. These plugins run during generation, making your changes repeatable and testable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling Your App: Three Approaches
&lt;/h2&gt;

&lt;p&gt;Coming from web development, you might wonder about styling options. React Native offers several approaches:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;StyleSheet (Built-in)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React Native's built-in styling API. Can be verbose for theming.&lt;/td&gt;
&lt;td&gt;Projects that want zero dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NativeWind&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tailwind CSS for React Native. Familiar utility classes.&lt;/td&gt;
&lt;td&gt;Web developers wanting quick start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unistyles&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Like StyleSheet but with better theming support (light/dark mode).&lt;/td&gt;
&lt;td&gt;Apps needing robust theming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; If you want your app to look distinctly "native" rather than like a website, study the Apple Human Interface Guidelines and Google Material Design specs. Expo UI (currently in beta) is also exposing more native components like segmented controls and glass buttons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment: From Code to App Store
&lt;/h2&gt;

&lt;p&gt;Deploying mobile apps is fundamentally different from web deployment. You need to handle code signing (certificates and provisioning profiles) and pass through a manual review process. Expo offers several services to streamline this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EAS Build&lt;/strong&gt;: Cloud-based CI/CD that builds your native apps. Particularly valuable because you need a Mac to build iOS apps — EAS Build provides that infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS Submit&lt;/strong&gt;: Uploads your built apps to the App Store and Play Store. Free to use and essential since Apple requires specific tools (Transporter) to upload apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS Update&lt;/strong&gt;: Push JavaScript updates without going through app store review. Fix bugs instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://launch.expo.dev/" rel="noopener noreferrer"&gt;Expo Launch&lt;/a&gt;&lt;/strong&gt;: A new beta product that takes a GitHub repo and handles everything — building, signing, and submission. Currently supports public repos and iOS (Play Store coming soon).&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Fpd0c9zzr192sexyz3na6.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.amazonaws.com%2Fuploads%2Farticles%2Fpd0c9zzr192sexyz3na6.png" alt="Screenshot 2026-01-22 at 12.30.28 PM.png" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Server Code in Your React Native App
&lt;/h2&gt;

&lt;p&gt;A powerful recent addition is the ability to write API routes directly in your React Native codebase. Create files ending in &lt;code&gt;+api.ts&lt;/code&gt; in your source folder:&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="c1"&gt;// src/posts+api.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from server!&lt;/span&gt;&lt;span class="dl"&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;Environment variables without the &lt;code&gt;EXPO_PUBLIC_&lt;/code&gt; prefix are only accessible in these server routes, keeping your secrets safe. Deploy with EAS Hosting and you have a full-stack mobile app with backend API routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-Assisted Development with Expo
&lt;/h2&gt;

&lt;p&gt;The Expo ecosystem is particularly well-suited for AI-assisted development. With Expo Go's instant feedback loop, tools like Replit and various AI coding assistants can help users go from idea to working app in minutes.&lt;/p&gt;

&lt;p&gt;However, there's an important distinction between "vibe coding" (letting AI write everything) and AI-assisted development. Even when using AI tools, you still own the code and are responsible for testing, iterating, and ensuring quality. AI can give you a starting point, but professional development still requires understanding what the code does.&lt;/p&gt;

&lt;p&gt;Resources like MCPs (Model Context Protocols), cursor rules, and Claude rules allow experts to codify best practices that AI tools can use as a source of truth — resulting in much higher quality generated code than generic training data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming Soon: Performance Observability
&lt;/h2&gt;

&lt;p&gt;Expo is developing an observability product for monitoring app performance — think Lighthouse scores but for native apps. You'll be able to track metrics like JavaScript bundle load time, see how different app versions affect performance, and identify slow devices or sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start Checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install Node.js&lt;/strong&gt; and create a new project with &lt;code&gt;npx create-expo-app@latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download Expo Go&lt;/strong&gt; on your phone for instant testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;npx expo start&lt;/code&gt;&lt;/strong&gt; and scan the QR code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the Expo docs&lt;/strong&gt; (docs.expo.dev) — they have an AI assistant built-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check React Native Directory&lt;/strong&gt; (reactnative.directory) for community libraries&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Additional Learning Resources
&lt;/h2&gt;

&lt;p&gt;To continue your learning journey, here are some excellent resources:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full Stack React Native Course by Tube Guruji&lt;/strong&gt;: A comprehensive 4+ hour course building a local business directory app from scratch using React Native, Expo, and a full backend. Perfect for hands-on learning. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Written Tutorial&lt;/strong&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Folsia0b9o07f0wnqks7l.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.amazonaws.com%2Fuploads%2Farticles%2Folsia0b9o07f0wnqks7l.png" alt="Your paragraph text.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="(https://strapi.io/blog/building-a-react-native-app-with-expo-and-strapi-a-complete-guide)"&gt;Building a React Native App with Expo and Strapi: A Complete Guide&lt;/a&gt;&lt;/strong&gt;: A detailed step-by-step tutorial from the Strapi blog covering project setup, NativeWind styling, data fetching with React Query, and building a complete blog app with infinite scroll. &lt;a href="(https://strapi.io/blog/building-a-react-native-app-with-expo-and-strapi-a-complete-guide)"&gt;Read it here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Expo has transformed React Native development from a complex, native-tooling-heavy process into something that feels almost as simple as web development. For React developers, there's never been a better time to start building mobile apps.&lt;/p&gt;

&lt;p&gt;The key takeaways: Start with Expo Go for learning, graduate to development builds for production, embrace CNG to avoid native folder maintenance, and leverage EAS services for painless deployment. With these tools, you can go from idea to app store in record time.&lt;/p&gt;

&lt;p&gt;Meta officially recommends Expo for React Native development, and it's clear why: the developer experience is exceptional, the ecosystem is mature, and the path from prototype to production is smoother than ever.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Happy building!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://strapi.io/blog/building-a-react-native-app-with-expo-and-strapi-a-complete-guide" rel="noopener noreferrer"&gt;strapi.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>react</category>
      <category>expo</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Introducing Yearly Plans and New Limits for Strapi Cloud</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 06 Feb 2026 12:34:03 +0000</pubDate>
      <link>https://dev.to/strapi/introducing-yearly-plans-and-new-limits-for-strapi-cloud-165l</link>
      <guid>https://dev.to/strapi/introducing-yearly-plans-and-new-limits-for-strapi-cloud-165l</guid>
      <description>&lt;p&gt;Back in March, we introduced a &lt;a href="https://strapi.io/blog/lower-prices-and-greater-flexibility-with-improved-strapi-cloud-pricing" rel="noopener noreferrer"&gt;new pricing model, making Strapi Cloud more accessible&lt;/a&gt;. The intention was simple: reduce friction for builders. We simplified Strapi Cloud pricing by offering “hosting-only plans” (without CMS features), increasing included storage, and removing CMS seat and entry limits. The result was a pricing model that grows with your business, where you only pay for what you need when you need it. This approach makes it easier for you to scale, allowing you to focus less on infrastructure management and more on building products that matter. &lt;/p&gt;

&lt;p&gt;Today, we’re taking the next step in that direction. We're introducing &lt;strong&gt;annual billing with 20% savings&lt;/strong&gt;, alongside refined usage limits that ensure each plan serves its intended purpose. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;20% discount for yearly billing compared to monthly billing&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refined usage limits:&lt;/strong&gt; Updated API request limits for Free and Essential plans&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly quota resets:&lt;/strong&gt; API requests and bandwidth renew monthly, even on yearly plans&lt;/li&gt;
&lt;li&gt;Explore the &lt;a href="https://strapi.io/pricing" rel="noopener noreferrer"&gt;&lt;strong&gt;Strapi Cloud Pricing page&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://support.strapi.io/articles/3581379360-understanding-strapi-cloud-usage-and-billing" rel="noopener noreferrer"&gt;&lt;strong&gt;Cloud billing &amp;amp; usage docs&lt;/strong&gt;&lt;/a&gt; for complete details&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s new and what it means for you
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Introducing Yearly Pricing&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;We heard from many customers that they prefer annual billing for simplified budgeting and reduced administrative overhead. One annual invoice means less time spent processing payments for both developers and accounting teams.&lt;/p&gt;

&lt;p&gt;You can now choose &lt;strong&gt;yearly billing&lt;/strong&gt; on all paid Strapi Cloud plans and get a &lt;strong&gt;20% discount&lt;/strong&gt; compared to monthly billing, with one annual invoice instead of twelve. &lt;/p&gt;

&lt;p&gt;Yearly plans offer the best value for teams that are ready to commit and plan ahead.  &lt;strong&gt;Monthly billing remains available&lt;/strong&gt; for teams that require maximum flexibility and the ability to cancel at any time, albeit at a higher monthly rate. This reflects the trade-off between flexibility and long-term savings, allowing you to choose what best suits your situation.&lt;/p&gt;

&lt;p&gt;Existing monthly customers will continue on their current plans without any changes to their pricing. You're grandfathered in at your existing rates. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly resets still apply on yearly plans too&lt;/strong&gt;, regardless of how you’re billed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API requests reset monthly&lt;/li&gt;
&lt;li&gt;Bandwidth resets monthly&lt;/li&gt;
&lt;li&gt;Asset storage remains an ongoing limit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're currently on a monthly plan, don't worry, you'll keep your current pricing. Your rates won't change, and you'll continue with the same limits you have today. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Updated API request limits&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;We’ve refined API request limits on two plans to better reflect how they’re typically used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free plan:&lt;/strong&gt; 2,500 API requests/month (previously 10,000)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Essential plan:&lt;/strong&gt; 50,000 API requests/month (previously 100,000) – &lt;em&gt;applies to new subscriptions only&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;Free plan&lt;/strong&gt; remains ideal for personal projects, learning, and early prototypes. These refined limits enable you to build in a stable environment, knowing exactly when your project is ready to transition from proof of concept to production. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Existing Essential customers are protected.&lt;/strong&gt; If you’re already on the &lt;strong&gt;Essential plan&lt;/strong&gt;, nothing changes. You’ll keep your existing &lt;strong&gt;100,000 API requests/month&lt;/strong&gt; limit. No action required. No disruption. We believe trust is built by honoring existing commitments and protecting the way you’re already building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we changed the limits
&lt;/h2&gt;

&lt;p&gt;To be clear and honest, &lt;strong&gt;lower limits aren’t a direct benefit to individual users&lt;/strong&gt;. These updates help keep Strapi Cloud reliable, sustainable, and aligned with how each plan is intended to be used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free plan:&lt;/strong&gt; lower limits ensure stability and usability for everyone experimenting or learning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform sustainability:&lt;/strong&gt; helps maintain performance, prevent abuse, and fund continued investment in new capabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your project outgrows a plan, that’s a signal it’s ready for the next stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next for Strapi Cloud
&lt;/h2&gt;

&lt;p&gt;Alongside these pricing updates, we're continuously building new capabilities across all paid Strapi Cloud plans designed to give you more visibility, control, and speed as your projects grow. &lt;/p&gt;

&lt;p&gt;This includes enhanced logging, monitoring, and other improvements focused on improving the developer experience. These initiatives are shaping the &lt;strong&gt;2026 roadmap&lt;/strong&gt;, and we'll share more details as they become available.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;FAQ&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Will yearly plans be available for the Growth (CMS) plan?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yearly billing currently applies only to &lt;strong&gt;Strapi Cloud hosting plans&lt;/strong&gt; (Essential, Pro, Scale). CMS feature licenses (Growth) remain separate for now and are available on a monthly basis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I stay on the Free plan and pay overages?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No, overages don't apply to the Free plan. If you need additional resources, upgrading to a paid plan starting at just $18/month (or $15/month with yearly billing) gives you significantly more capacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do monthly quotas reset monthly, even on yearly plans?&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Yes. API requests and bandwidth renewals occur on a monthly basis on yearly plans. Unlike Asset storage, which is an ongoing limit. For example, an Essential customer with 100,000 API requests/month makes 125,000 requests in a given month. They exceeded their quota by 25,000 API requests and would be charged an additional $1.50. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I switch from monthly to yearly billing and vice versa?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, you can adjust your billing frequency in your dashboard at any point in time. However, the change will take effect at the end of your current billing cycle. For yearly plans, this means the switch will occur after your 12-month commitment has been completed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Strapi Cloud better than managing my own servers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In most cases, yes, it greatly reduces operational overhead and lets you build faster. If you require full infrastructure control or want to keep both your frontend and backend on the same infrastructure, self-hosting remains an option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to existing Essential customers with the new limits?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Current Essential customers keep their existing 100,000 API requests/month limit and won't be affected by these changes. The new 50,000 API requests/month limit only applies to new Essential subscriptions created after &lt;strong&gt;December 22nd, 2025&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://strapi.io/blog/introducing-yearly-plans-and-new-limits-for-strapi-cloud" rel="noopener noreferrer"&gt;strapi.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>strapi</category>
      <category>cloud</category>
      <category>strapicloud</category>
    </item>
    <item>
      <title>Chat as the New Interface: Building for the Agentic Web with Kent C. Dodds</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 06 Feb 2026 11:56:41 +0000</pubDate>
      <link>https://dev.to/strapi/chat-as-the-new-interface-building-for-the-agentic-web-with-kent-c-dodds-4pfn</link>
      <guid>https://dev.to/strapi/chat-as-the-new-interface-building-for-the-agentic-web-with-kent-c-dodds-4pfn</guid>
      <description>&lt;p&gt;I recently had the chance to sit down with one of my favorite educators in web development, Kent — someone whose courses and talks played a big role in my own late-career transition into tech. We started out talking about React Server Components… and somehow ended up in a full-on conversation about the future of the web, AI agents, and why Model Context Protocol (MCP) might be as big a shift as the early days of the internet.&lt;/p&gt;

&lt;p&gt;You can watch the whole talk here, or keep reading for everything we covered.&lt;/p&gt;




&lt;h2&gt;
  
  
  Users Are Quietly Leaving the Browser
&lt;/h2&gt;

&lt;p&gt;Kent said something that really stuck with me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Users are fleeing the browser and moving to AI agents.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you think about your own habits, that might already be true.&lt;/p&gt;

&lt;p&gt;I realized I barely use Google anymore. Most of the time I don’t &lt;em&gt;search&lt;/em&gt; for a page — I &lt;em&gt;ask&lt;/em&gt; an AI a question.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finding the right keywords&lt;/li&gt;
&lt;li&gt;Clicking through SEO’d blog posts&lt;/li&gt;
&lt;li&gt;Hunting for the one paragraph that actually answers my question&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…I just open ChatGPT or Claude and type what I want.&lt;/p&gt;

&lt;p&gt;That’s not just a nicer UX. It’s a fundamental shift in the &lt;em&gt;primary interface&lt;/em&gt; between users and software:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: users went to &lt;strong&gt;websites&lt;/strong&gt; and &lt;strong&gt;apps&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Now: users go to &lt;strong&gt;agents&lt;/strong&gt;, and agents go to websites and apps &lt;em&gt;for them&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browsers and search engines are trying to adapt (AI answers on search result pages), and AI tools are trying to adapt (chat + files + tools + browsing), but the direction is pretty clear:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The main way people interact with software is becoming &lt;strong&gt;“Asking”&lt;/strong&gt;, not “Searching”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  ChatGPT Apps and the New Interaction Model
&lt;/h2&gt;

&lt;p&gt;At React Conf, Kent showed a short promo video for an upcoming ChatGPT feature: &lt;strong&gt;ChatGPT apps&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of going to a website and logging in&lt;/li&gt;
&lt;li&gt;You tell ChatGPT what you want to do&lt;/li&gt;
&lt;li&gt;ChatGPT decides which “app” (backed by tools/APIs) is best to handle it&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;That app appears &lt;em&gt;inside the chat&lt;/em&gt;, as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small widget&lt;/li&gt;
&lt;li&gt;A full-screen experience&lt;/li&gt;
&lt;li&gt;A hybrid where you both &lt;strong&gt;click UI&lt;/strong&gt; and &lt;strong&gt;keep chatting&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Input becomes &lt;strong&gt;multimodal&lt;/strong&gt;: text, voice, clicks, typing. The agent chooses whatever mix of UI + conversation makes the most sense for the task.&lt;/p&gt;

&lt;p&gt;The important mental shift:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In the future, you won’t just be designing for “the user in the browser.”&lt;br&gt;
You’ll be designing for &lt;strong&gt;the user’s agent&lt;/strong&gt;, which is acting on their behalf.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That means your app needs a way to say to the agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Here’s what I can do. Here are the actions I support. Here’s how to call me.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s where &lt;strong&gt;MCP&lt;/strong&gt; comes in.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is MCP, Really?
&lt;/h2&gt;

&lt;p&gt;MCP stands for &lt;strong&gt;Model Context Protocol&lt;/strong&gt;. Under all the buzzwords, it’s basically:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A standard way for AI agents (like ChatGPT, Claude, etc.) to talk to external services in a structured, predictable way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You expose your app as an &lt;strong&gt;MCP server&lt;/strong&gt;, which defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; – the operations your app can perform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inputs/outputs&lt;/strong&gt; – what arguments those tools need and what they return&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; – how the agent is allowed to call those tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then an agent can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read your tool definitions&lt;/li&gt;
&lt;li&gt;Decide when to call them based on what the user asks&lt;/li&gt;
&lt;li&gt;Chain them together with other tools, web search, memory, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Kent has been focusing a lot on &lt;strong&gt;consumer-oriented MCP servers&lt;/strong&gt; (not just dev tools in an IDE). One of his example projects is a journaling app, exposed via MCP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You could talk to ChatGPT and say,
&lt;em&gt;“Help me journal about my day and save it.”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;The agent asks you questions, writes the entry, and stores it via the MCP server.&lt;/li&gt;
&lt;li&gt;You interact with &lt;strong&gt;ChatGPT&lt;/strong&gt;, not “yet another journaling app UI,” but your data still lives in your service, with your logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the pattern:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your app becomes a &lt;strong&gt;capability provider&lt;/strong&gt; to agents, not just a website with a login form.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  “If You’re Not Integrated, You’ll Be Left Behind”
&lt;/h2&gt;

&lt;p&gt;Kent compared this shift to the early web:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the 90s, a lot of businesses didn’t think they needed a website.&lt;/li&gt;
&lt;li&gt;By 2010, not having one made you basically invisible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;He sees MCP and agent integration the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Today, most companies are still asking:
&lt;em&gt;“Why would my service need an MCP server?”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Tomorrow, the question becomes:
&lt;em&gt;“Why on earth *don’t&lt;/em&gt; you have one?”*&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ChatGPT or Claude has hundreds of millions of users.&lt;/li&gt;
&lt;li&gt;Those users say, &lt;em&gt;“Book me a ride,” “Find me a sitter,” “Order me this thing,” “Update my account,” “Change my subscription,”&lt;/em&gt; etc.&lt;/li&gt;
&lt;li&gt;The agent looks at all the available services exposed via MCP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your competitor has MCP integration and you don’t, the agent will prefer the one it can seamlessly call. Browsing your website and scraping forms will always be a worse UX than &lt;strong&gt;direct integration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Yes, agents can technically browse the web. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It’s slower&lt;/li&gt;
&lt;li&gt;Less reliable&lt;/li&gt;
&lt;li&gt;Harder to secure&lt;/li&gt;
&lt;li&gt;Worse for the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agents are incentivized to use &lt;strong&gt;directly integrated tools&lt;/strong&gt; whenever possible. That’s why Kent argues:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ignoring MCP and agent integration will eventually feel like not having a website in 2010.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What Happens to UI and Frontend Work?
&lt;/h2&gt;

&lt;p&gt;Does this mean UI is dead? Not at all.&lt;/p&gt;

&lt;p&gt;Kent thinks custom UI will be important &lt;strong&gt;for at least the next five years&lt;/strong&gt; (and probably longer — but forecasting beyond that gets dicey fast).&lt;/p&gt;

&lt;p&gt;A few key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Branding still matters&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you ask your agent, &lt;em&gt;“Get me a ride to the airport,”&lt;/em&gt;
Uber cares &lt;em&gt;a lot&lt;/em&gt; that you know it’s &lt;strong&gt;Uber&lt;/strong&gt; picking you up, not some random service.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Trust still matters&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users want to know which company they’re actually interacting with.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;UI libraries may become “building blocks”&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the future, a service might expose:&lt;/li&gt;
&lt;li&gt;Not just tools (actions)&lt;/li&gt;
&lt;li&gt;But also &lt;strong&gt;UI components&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The agent could then compose those components into custom layouts based on what the user is trying to do.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;We may eventually see agents generate full UI on the fly, but we’re not there yet. For now:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Frontend work is still about building components, flows, and branded experiences — just increasingly in collaboration with agents instead of only with browsers.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  “Is AI Going to Take My Job?”
&lt;/h2&gt;

&lt;p&gt;We had to talk about the big fear: &lt;em&gt;AI replacing developers&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Kent’s take was both simple and kind of liberating:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If&lt;/strong&gt; AI completely replaces all developers in, say, a year&lt;/li&gt;
&lt;li&gt;There is &lt;strong&gt;nothing&lt;/strong&gt; you can do to meaningfully prepare&lt;/li&gt;
&lt;li&gt;No skill, no amount of money, no clever trick changes that outcome&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So planning your life around that scenario is pointless. You can’t act on it.&lt;/p&gt;

&lt;p&gt;Now remove that from the table and ask a more useful question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Assuming AI remains a powerful tool, not an instant replacement, how do I stay relevant and valuable?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;His answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get really good at &lt;strong&gt;using AI&lt;/strong&gt; in your workflow&lt;/li&gt;
&lt;li&gt;Get really good at &lt;strong&gt;integrating AI&lt;/strong&gt; into your products&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And this matches my own experience too. I’ve used AI to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate large chunks of code quickly&lt;/li&gt;
&lt;li&gt;Create entire prototypes with 3D graphics&lt;/li&gt;
&lt;li&gt;Iterate on ideas at a speed that wasn’t possible before&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here’s the catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI can often get you &lt;strong&gt;60–80% of the way&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;That last &lt;strong&gt;20% is brutal&lt;/strong&gt; if you don’t understand what’s going on&lt;/li&gt;
&lt;li&gt;Debugging, performance tuning, architecture decisions, tradeoffs — that’s still on &lt;em&gt;you&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The skills that matter more than ever:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Problem solving&lt;/li&gt;
&lt;li&gt;Debugging and troubleshooting&lt;/li&gt;
&lt;li&gt;Understanding systems and tradeoffs&lt;/li&gt;
&lt;li&gt;Being able to explain your problem clearly (to humans &lt;em&gt;and&lt;/em&gt; AI)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kent even tries to talk to LLMs the same way he’d talk to another engineer — not just “do this,” but with context, constraints, and reasoning. That habit itself is a valuable engineering skill.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Kent Uses AI Day-to-Day
&lt;/h2&gt;

&lt;p&gt;A few practical bits from Kent’s workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;He uses &lt;strong&gt;Cursor&lt;/strong&gt; as his primary editor&lt;/li&gt;
&lt;li&gt;He still likes &lt;strong&gt;editor mode&lt;/strong&gt; over a pure chat agent — seeing and editing files is part of his “prompt”&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He treats the existing codebase as &lt;strong&gt;context for the model&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the AI keeps giving wrong answers&lt;/li&gt;
&lt;li&gt;He improves the code structure and patterns&lt;/li&gt;
&lt;li&gt;So future suggestions become better&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;He also uses ChatGPT outside the editor to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Think through ideas&lt;/li&gt;
&lt;li&gt;Step away from the code and reason at a higher level&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, AI isn’t a replacement for thought — it’s a &lt;strong&gt;partner in thinking&lt;/strong&gt; and typing.&lt;/p&gt;




&lt;h2&gt;
  
  
  MCP vs “Yet Another AI Wrapper App”
&lt;/h2&gt;

&lt;p&gt;One of the most interesting parts of the conversation was about the current wave of “AI apps.”&lt;/p&gt;

&lt;p&gt;For the past few years, companies have been building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chatbots that sit in front of their knowledge bases&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RAG (retrieval augmented generation) systems that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Take a user question&lt;/li&gt;
&lt;li&gt;Search internal docs&lt;/li&gt;
&lt;li&gt;Stuff the results into the prompt&lt;/li&gt;
&lt;li&gt;Ask a model to answer&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Those are useful, but they have big limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They only know what’s inside &lt;em&gt;your&lt;/em&gt; knowledge base&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;They don’t have the broader capabilities of ChatGPT/Claude:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web search&lt;/li&gt;
&lt;li&gt;Other tools&lt;/li&gt;
&lt;li&gt;Memory&lt;/li&gt;
&lt;li&gt;General world knowledge&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;If instead you expose your knowledge base (and other capabilities) via MCP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ChatGPT can combine your data with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Other services&lt;/li&gt;
&lt;li&gt;Search&lt;/li&gt;
&lt;li&gt;The user’s history and preferences&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;You don’t have to maintain a completely separate chat UX if you don’t want to&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;You ride the wave as agents get smarter and more integrated&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;There &lt;em&gt;are&lt;/em&gt; still valid reasons to build your own “wrapper”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need something immediately&lt;/li&gt;
&lt;li&gt;You can’t (or don’t want to) rely on ChatGPT / Claude’s current MCP support&lt;/li&gt;
&lt;li&gt;You have strict privacy constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But even then, Kent recommends:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build your &lt;strong&gt;own chatbot&lt;/strong&gt; on top of an &lt;strong&gt;MCP server&lt;/strong&gt; you control.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internally: your bot talks to your MCP server&lt;/li&gt;
&lt;li&gt;Externally (later): ChatGPT, Claude, or any other agent can talk to that &lt;em&gt;same&lt;/em&gt; MCP server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You get both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Control over privacy and UX&lt;/li&gt;
&lt;li&gt;Future compatibility with general-purpose agents&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common MCP Misconceptions
&lt;/h2&gt;

&lt;p&gt;A few myths Kent keeps running into:&lt;/p&gt;

&lt;h3&gt;
  
  
  “MCP doesn’t support OAuth.”
&lt;/h3&gt;

&lt;p&gt;It does. And the spec has been improving quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Earlier versions made the MCP server act as its own OAuth server (painful)&lt;/li&gt;
&lt;li&gt;Newer versions treat it as a &lt;strong&gt;resource server&lt;/strong&gt; (much simpler)&lt;/li&gt;
&lt;li&gt;Recent updates made it even more secure and easier to work with&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Is it perfect? No. But it’s absolutely not “no auth.”&lt;/p&gt;

&lt;h3&gt;
  
  
  “Why not just use a CLI? The agent can call that.”
&lt;/h3&gt;

&lt;p&gt;For developer tools, sure — a CLI might be fine.&lt;/p&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CLIs are &lt;strong&gt;non-standard&lt;/strong&gt; — every one is different&lt;/li&gt;
&lt;li&gt;Agents would have to repeatedly run &lt;code&gt;--help&lt;/code&gt; and parse text&lt;/li&gt;
&lt;li&gt;That’s slow, brittle, and not consumer-friendly&lt;/li&gt;
&lt;li&gt;And your actual users are not going to be using CLIs on their phones by voice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MCP gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;standard tool definition&lt;/strong&gt; format&lt;/li&gt;
&lt;li&gt;A way for agents to &lt;strong&gt;optimize&lt;/strong&gt; how they load and call tools&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Something that works across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dev tools&lt;/li&gt;
&lt;li&gt;Consumer apps&lt;/li&gt;
&lt;li&gt;Mobile devices&lt;/li&gt;
&lt;li&gt;Voice interfaces&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  So… What Should You Actually Do?
&lt;/h2&gt;

&lt;p&gt;If you’re a developer or technical founder, here’s a simple roadmap pulled from our conversation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accept that agents are becoming the new frontend.&lt;/strong&gt;&lt;br&gt;
Users will increasingly talk to AI first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep investing in UI skills — they still matter.&lt;/strong&gt;&lt;br&gt;
Branding, trust, and good interaction design aren’t going away.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Get good at using AI in your workflow.&lt;/strong&gt;&lt;br&gt;
Treat it like a powerful teammate that’s great at drafting, exploring, and refactoring.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start learning about MCP.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Read the spec&lt;/li&gt;
&lt;li&gt;Look at existing open-source MCP servers&lt;/li&gt;
&lt;li&gt;Try building a tiny one yourself for a hobby project or side tool&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Think in terms of “capabilities,” not just “apps.”&lt;/strong&gt;
Ask:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;“What can my service &lt;em&gt;do&lt;/em&gt; for a user?”&lt;/li&gt;
&lt;li&gt;“How can I expose those actions to an agent cleanly?”&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stay optimistic.&lt;/strong&gt;
Ignoring AI doesn’t protect you — it just puts you behind. Integrating it makes &lt;em&gt;you&lt;/em&gt; more capable, not obsolete.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Kent closed our conversation with this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Be optimistic. The version of you that learns to use and integrate AI will be better than the version of you who doesn’t.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I tend to agree.&lt;/p&gt;

&lt;p&gt;You don’t need to predict the entire future of AI.&lt;br&gt;
You don’t need to know exactly where we’ll be in ten years.&lt;/p&gt;

&lt;p&gt;But you &lt;em&gt;can&lt;/em&gt; start today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use agents more intentionally&lt;/li&gt;
&lt;li&gt;Build small integrations&lt;/li&gt;
&lt;li&gt;Ship a simple MCP server&lt;/li&gt;
&lt;li&gt;And keep your skills pointed toward where the puck is going — not where it’s been.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://strapi.io/blog/chat-as-the-new-interface-building-for-the-agentic-web-with-kent-c-dodds" rel="noopener noreferrer"&gt;strapi.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>agenticai</category>
      <category>community</category>
    </item>
    <item>
      <title>Quality First: Our Commitment to a (Even) More Robust Strapi</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 06 Feb 2026 11:53:17 +0000</pubDate>
      <link>https://dev.to/strapi/quality-first-our-commitment-to-a-even-more-robust-strapi-3604</link>
      <guid>https://dev.to/strapi/quality-first-our-commitment-to-a-even-more-robust-strapi-3604</guid>
      <description>&lt;p&gt;&lt;strong&gt;v5.31.1: 24 bug fixes. v5.30.1: 12 bug fixes. v5.29.0: 20 stability improvements.&lt;/strong&gt; The pattern is clear, quality is now our top priority until the end of the year and throughout 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters?
&lt;/h2&gt;

&lt;p&gt;We shipped significant features in 2025, including TypeScript 5.0 support, Responsive Admin Panel, &lt;a href="https://strapi.io/features/conditional-fields" rel="noopener noreferrer"&gt;Conditional Fields&lt;/a&gt;, &lt;a href="https://strapi.io/features/live-preview" rel="noopener noreferrer"&gt;Live Preview&lt;/a&gt;, &lt;a href="https://docs.strapi.io/cms/admin-panel-customization/homepage" rel="noopener noreferrer"&gt;Homepage Customization&lt;/a&gt;, Database Transactions, Draft &amp;amp; Publish improvements, &lt;a href="https://strapi.io/ai" rel="noopener noreferrer"&gt;Strapi AI&lt;/a&gt;, Shopify/Cloudinary/BigCommerce integrations, and more. But velocity came at a cost to stability.&lt;/p&gt;

&lt;p&gt;You've been clear through &lt;a href="https://github.com/strapi/strapi/issues/24956" rel="noopener noreferrer"&gt;GitHub issues&lt;/a&gt; and &lt;a href="https://feedback.strapi.io/" rel="noopener noreferrer"&gt;community feedback&lt;/a&gt;. The top-voted requests aren't flashy new features; they're reliability fundamentals. Better error handling, performance optimization, data transfer, and media library stability. Support tickets echo the same patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did we get there?
&lt;/h2&gt;

&lt;p&gt;We're a small team maintaining three products that serve different needs: Strapi Community Edition (open source), Strapi Enterprise Edition, and Strapi Cloud. Each one requires attention, resources, and care. As an open-source company, we face a constant balancing act: generating revenue to sustain the project while staying true to our commitment to the community that makes Strapi what it is.&lt;/p&gt;

&lt;p&gt;It's not easy. When we prioritize one, the other can slip. And we know you've felt that in some ways, lingering bugs, features that would benefit from additional iterations, or pull requests that go unreviewed for some time. &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.amazonaws.com%2Fuploads%2Farticles%2F3m7qd5g87f4mvz9uwnkb.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.amazonaws.com%2Fuploads%2Farticles%2F3m7qd5g87f4mvz9uwnkb.png" alt="GitHub PR" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are doing about it
&lt;/h2&gt;

&lt;p&gt;Recent releases show the shift. &lt;a href="https://dev.to323"&gt;v5.31&lt;/a&gt;, &lt;a href="https://github.com/strapi/strapi/releases/tag/v5.30.1" rel="noopener noreferrer"&gt;v5.30&lt;/a&gt;, and &lt;a href="https://github.com/strapi/strapi/releases/tag/v5.29.0" rel="noopener noreferrer"&gt;v5.29&lt;/a&gt; merged over 70 PRs focused on quality. &lt;/p&gt;

&lt;p&gt;Here is a summary of these updates across various parts of the codebase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Content Modeling:&lt;/strong&gt; &lt;a href="https://github.com/strapi/strapi/pull/24056" rel="noopener noreferrer"&gt;Fixed schema generation with negative numbers&lt;/a&gt;, &lt;a href="https://github.com/strapi/strapi/pull/24708" rel="noopener noreferrer"&gt;resolved enum defaults&lt;/a&gt;, and &lt;a href="https://github.com/strapi/strapi/pull/24566" rel="noopener noreferrer"&gt;Content-Type Builder accessibility&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Manager:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Content Internationalization: Fixed &lt;a href="https://github.com/strapi/strapi/pull/24605" rel="noopener noreferrer"&gt;field flickering on i18n rerenders&lt;/a&gt;, &lt;a href="https://github.com/strapi/strapi/pull/24878" rel="noopener noreferrer"&gt;bulk&lt;/a&gt; &lt;a href="https://github.com/strapi/strapi/pull/24821" rel="noopener noreferrer"&gt;unpublish with new locales&lt;/a&gt;, &lt;a href="https://github.com/strapi/strapi/pull/24785" rel="noopener noreferrer"&gt;draft relation counts&lt;/a&gt;, &lt;a href="https://github.com/strapi/strapi/pull/22417" rel="noopener noreferrer"&gt;locale picker ordering&lt;/a&gt;, &lt;a href="https://github.com/strapi/strapi/pull/24821" rel="noopener noreferrer"&gt;publish/unpublish with locales&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Responsiveness: improved &lt;a href="https://github.com/strapi/strapi/pull/24765" rel="noopener noreferrer"&gt;responsiveness of the left menu&lt;/a&gt; and &lt;a href="https://github.com/strapi/strapi/pull/24616" rel="noopener noreferrer"&gt;subnav&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;General Fixes: &lt;a href="https://github.com/strapi/strapi/pull/24849" rel="noopener noreferrer"&gt;preserved hidden fields during form submission&lt;/a&gt;, &lt;a href="https://github.com/strapi/strapi/pull/24739" rel="noopener noreferrer"&gt;admin panel safety (prevented accidental self-deletion)&lt;/a&gt;, &lt;a href="https://github.com/strapi/strapi/pull/21738" rel="noopener noreferrer"&gt;dynamic zone configurations&lt;/a&gt;, and &lt;a href="https://github.com/strapi/strapi/pull/21792" rel="noopener noreferrer"&gt;bulk operations&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media Library &amp;amp; Upload:&lt;/strong&gt; Resolved &lt;a href="https://github.com/strapi/strapi/pull/24633" rel="noopener noreferrer"&gt;modal click issues&lt;/a&gt;, &lt;a href="https://github.com/strapi/strapi/pull/24573" rel="noopener noreferrer"&gt;file-type detection&lt;/a&gt;,  &lt;a href="https://github.com/strapi/strapi/pull/24533" rel="noopener noreferrer"&gt;folder selection&lt;/a&gt;, and &lt;a href="https://github.com/strapi/strapi/pull/23475" rel="noopener noreferrer"&gt;enhanced asset selection with accessibility improvements&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Developer Experience:&lt;/strong&gt; Improved &lt;a href="https://github.com/strapi/strapi/pull/24845" rel="noopener noreferrer"&gt;TypeScript errors&lt;/a&gt;, &lt;a href="https://github.com/strapi/strapi/pull/22852" rel="noopener noreferrer"&gt;customized CORS settings in the GraphQL plugin&lt;/a&gt;, and &lt;a href="https://github.com/strapi/strapi/pull/24586" rel="noopener noreferrer"&gt;Node 24 support added&lt;/a&gt;.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Quality isn't just our focus at the core; it extends to everything we do, from the documentation to the Marketplace and the LaunchPad demo application:&lt;/p&gt;

&lt;p&gt;Here are the latest updates from the M*&lt;em&gt;arketplace&lt;/em&gt;*: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We have migrated numerous Strapi plugins from Strapi 4 to Strapi 5 i.e plugin-rest-cache,&lt;/li&gt;
&lt;li&gt;We've removed outdated v4 plugins, eliminated duplicate listings, and increased visibility for higher-quality plugins, while also adding visibility warnings for unmaintained packages.&lt;/li&gt;
&lt;li&gt;Stale plugins (no updates in 2+ years) are being deprecated. Moving forward, we plan to raise quality standards and work toward automated compatibility testing with each Strapi release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ve also been busy reworking our &lt;strong&gt;documentation.&lt;/strong&gt; To name a few recent improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We added a new section on &lt;a href="https://www.strapi.io/e3t/Ctc/5D+113/cQNTM04/VWsDC022RMSMW2S5GJt6sCjK_W1k1H6J5G65gzN6J4pXn5nR3bW50kH_H6lZ3lsW4R6wKT4kdDFSW7K7LLD2XMH8CW5NDPr2829n8DW4-H9_z7JsVtqW3gLkFz5sGHVgW3fFbdh2SxBWfW1RRgKZ6sqfBrW7R5-NX6h-lq_W3j9Bgv2tHTgBW3mPc__7_HxwlN97RTSp89mgbW3ylrCN7vwvl8W39g6_27M3ZqGW28HNwp66669sW892-Cb8DZQ7CW5bt9W56KrGRVW4cM2TZ9lYD7yN4Vn4dPYLj8NW2kv6TS3SBgh7W93GWJb4dLYJVW7kD35-1njH2NN3bJ7Fq6xrpRW7Lm8Rb3dRR06W44s6w98CdZYQN15ZnJf3J0hXW4qhcV73GBT65W9gHFPp35n6S6N2FPDb4y5JmWW6XH67Z29XMHZW4H5Bq65q2nP4W5C7cwZ4XYv7dVNxkcg8XH3ZPf5r_Y_204" rel="noopener noreferrer"&gt;How to create admin permissions from plugins&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;We developed the &lt;a href="https://www.strapi.io/e3t/Ctc/5D+113/cQNTM04/VWsDC022RMSMW2S5GJt6sCjK_W1k1H6J5G65gzN6J4pYT3qgz0W7Y8-PT6lZ3mqW87YrJ61FBwsgN2LvP75L-RlXN2sfW4Q4B5vnW1fbbmB5kwm52W5CYLQ54Y88jKW6d5DTH8RDC7FW3HBdf-4d4pTHW6FSJKC7Dzq8tW3NQZBq8fgM9pW9fVLLB1tNCMsW10q7n-7h4ZnFW77TK6q4TphddW61lTVt6V_z1qW78VkkZ2WQ9D0W7W6nTL6X3nkfW7FlFSx3tfw--W4S3WT33JkbGkW4dJz0l3b0W40N6c9pStCKsJpN4Ym1bJ4H2bCW1rWrmX4tbxknW989vXD8yCc_gW93hBgb6LRHmzW5p_GGZ94QT6_W7PdfwJ1SMmSQW7Y1xhj7H64ZCdJ8Stq04" rel="noopener noreferrer"&gt;Information on aggregation in GraphQL API&lt;/a&gt; and &lt;a href="https://www.strapi.io/e3t/Ctc/5D+113/cQNTM04/VWsDC022RMSMW2S5GJt6sCjK_W1k1H6J5G65gzN6J4pZs3qgz0W95jsWP6lZ3pBW93vXl63pkRVrW3zY3CR2_1qVNVgRZL18VnczzW3bPBt71bmm_hW5Ljqvx3VCyWFW7RTg1770bVHfW69F2SZ6mm5RjVB2vTW1PMgTNW2C8r6H40qpvnN5R1L6zdzD_jN2p8W9QX597TW7qYHRX3-CR18W2_5h-x454ffKW7SV1Nb7VyhzlW6tCg_k62gsvmN78xF0SYvBSVW1GP-j_1zlcr6W7rTnC78DVv8fW6qdkLs3bTbRtTFymJ5Xm3WCN2-tJWKFz90lW7_WKV42RNfRQW4T8pNh4BjNLRW6k8HL872LybKN2fJnPLSfZStW8dPC836y2HPBV65S2x75LL37W8r3Cff6Fb01PVQh2Xp2rdGv8N4f8KZwc1rjbf4_fsMM04" rel="noopener noreferrer"&gt;GraphQL API related v5 migration&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;We reworked the &lt;a href="https://docs.strapi.io/cms/api/document-service" rel="noopener noreferrer"&gt;Document Service API page&lt;/a&gt; to better explain &lt;code&gt;documentId&lt;/code&gt; and available methods.&lt;/li&gt;
&lt;li&gt;We improved the &lt;a href="https://docs.strapi.io/cms/configurations/functions" rel="noopener noreferrer"&gt;Lifecycle functions reference&lt;/a&gt; with more explanations and examples.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  We need your help
&lt;/h2&gt;

&lt;p&gt;Here's the truth, though: &lt;strong&gt;We need your help.&lt;/strong&gt; We're prioritizing quality and committing resources, but we can't do this alone. Community contributions such as detailed issues, PRs, and documentation updates make a massive difference. &lt;/p&gt;

&lt;p&gt;For instance, last month's releases included 9 merged PRs from a diverse group of community contributors in countries like Colombia, India, Egypt, and Austria. Here are some of the amazing people who contributed: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ayush &lt;a href="https://github.com/Ayushd785" rel="noopener noreferrer"&gt;&lt;strong&gt;@Ayushd785&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chaitya &lt;a href="https://github.com/chaitya-titan" rel="noopener noreferrer"&gt;&lt;strong&gt;@chaitya-titan&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Daniel Garcia &lt;a href="https://github.com/dagadevelop" rel="noopener noreferrer"&gt;&lt;strong&gt;@dagadevelop&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Dmitry Maklygin &lt;a href="https://github.com/dmaklygin" rel="noopener noreferrer"&gt;&lt;strong&gt;@dmaklygin&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;José Luis &lt;a href="https://github.com/SalahAdDin" rel="noopener noreferrer"&gt;&lt;strong&gt;@SalahAdDin&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ndaemy &lt;a href="https://github.com/ndaemy" rel="noopener noreferrer"&gt;&lt;strong&gt;@ndaemy&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Razim Saidov &lt;a href="https://github.com/razims" rel="noopener noreferrer"&gt;&lt;strong&gt;@razims&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sam Phillemon &lt;a href="https://github.com/Sam-Phillemon9493" rel="noopener noreferrer"&gt;&lt;strong&gt;@Sam-Phillemon9493&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Fyjsdmg57u0802huqttat.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.amazonaws.com%2Fuploads%2Farticles%2Fyjsdmg57u0802huqttat.png" alt="Slide 16_9 - 70 (2).png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The unglamorous work matters most. In open source, it's the steady contributors who review PRs, triage issues, update docs, and fix bugs that build reliable software. We see you. We value you. Making contributions count requires effort from both sides. We're making a significant push to review PRs more frequently and merge them faster. The entire team is now pitching in to support this effort. On the contributor side, following the guidelines carefully makes a huge difference—writing clear descriptions, linking related issues, including tests, and providing all the necessary context helps us merge contributions quickly and efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why should you contribute to Strapi?
&lt;/h2&gt;

&lt;p&gt;Building on our promising &lt;a href="https://www.notion.so/Documentation-Contribution-Program-1d08f359807480d480fdde68bb7a5a71?pvs=21" rel="noopener noreferrer"&gt;Documentation Contribution Program&lt;/a&gt;, we plan to provide stronger recognition and clearer incentives to code contributors, including Strapi goodies from the &lt;a href="http://shop.strapi.io/" rel="noopener noreferrer"&gt;Strapi Shop&lt;/a&gt; and more! &lt;/p&gt;

&lt;p&gt;We're also planning to start allocating unused &lt;a href="https://opencollective.com/strapi#category-BUDGET" rel="noopener noreferrer"&gt;Open Collective funds&lt;/a&gt; directly toward contributions to the core codebase and popular plugins currently hosted under the &lt;a href="https://github.com/strapi-community/" rel="noopener noreferrer"&gt;Strapi Community GitHub Org&lt;/a&gt;: such as the plugin-rest-cache, strapi-plugin-sitemap, strapi-plugin-import-export-entries, strapi-plugin-seo, and strapi-plugin-menus! &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.amazonaws.com%2Fuploads%2Farticles%2Fd8k3uv2jfzutlduu73xn.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.amazonaws.com%2Fuploads%2Farticles%2Fd8k3uv2jfzutlduu73xn.png" alt="Screenshot 2025-12-03 at 9.44.18 PM.png" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're still finalizing the details of this program, so please stay tuned for more information. In the meantime, we invite you to check out our &lt;a href="https://github.com/strapi/strapi" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; and look for issues or PRs that could use some love! &lt;/p&gt;

&lt;h2&gt;
  
  
  Looking ahead to 2026
&lt;/h2&gt;

&lt;p&gt;In summary, our plan is to prioritize the foundations that directly impact reliability and stability. We'll tackle as many of these topics as possible, with progress dependent on both our internal resources and community contributions. &lt;/p&gt;

&lt;p&gt;Main focus areas currently include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Media Library performance and reliability&lt;/li&gt;
&lt;li&gt;Bulk operations reliability&lt;/li&gt;
&lt;li&gt;Relations handling&lt;/li&gt;
&lt;li&gt;Filters and search functionality&lt;/li&gt;
&lt;li&gt;Performance improvements include faster build times, optimized rendering, and more efficient compilation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every year, we ask the community to identify the issues that matter most to them. &lt;a href="https://github.com/strapi/strapi/issues/24956" rel="noopener noreferrer"&gt;Join the discussion on GitHub&lt;/a&gt; and tell us what we should prioritize. &lt;/p&gt;

&lt;p&gt;Your feedback and contributions directly shape our roadmap!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://strapi.io/blog/commitment-to-a-even-more-robust-strapi" rel="noopener noreferrer"&gt;strapi.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>strapi</category>
      <category>community</category>
    </item>
    <item>
      <title>Next.js 16 Route Handlers Explained: 3 Advanced Use Cases</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 06 Feb 2026 11:49:13 +0000</pubDate>
      <link>https://dev.to/strapi/nextjs-16-route-handlers-explained-3-advanced-use-cases-26d0</link>
      <guid>https://dev.to/strapi/nextjs-16-route-handlers-explained-3-advanced-use-cases-26d0</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Route handlers in &lt;a href="https://strapi.io/integrations/nextjs-cms" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; 13.2+ provide a cleaner, platform-native alternative to legacy API Routes. They use the standard &lt;strong&gt;&lt;code&gt;Request&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;Response&lt;/code&gt;&lt;/strong&gt; Web APIs and make it easy to build server-side logic and REST endpoints with familiar HTTP methods like &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Beyond simple CRUD, route handlers are ideal for tasks that don’t belong in React components, such as file generation, data transformation, proxying, and other backend-for-frontend responsibilities.&lt;/p&gt;

&lt;p&gt;This article walks through 3 production-ready use cases that show how to apply route handlers effectively in real Next.js applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use Next.js Route Handlers?
&lt;/h2&gt;

&lt;p&gt;Route handlers are ideal whenever your application needs server-side logic that sits outside the React rendering lifecycle. In practice, they’re a great fit for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shared API logic with Higher-Order Functions (HOFs)&lt;/strong&gt;: Apply logging, validation, authorization, or context forwarding across multiple &lt;a href="https://strapi.io/blog/guide-on-authenticating-requests-with-the-rest-api" rel="noopener noreferrer"&gt;REST endpoints&lt;/a&gt; without repeating code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend responsibilities without a backend server&lt;/strong&gt;: Route handlers can perform tasks such as:

&lt;ul&gt;
&lt;li&gt;Generating downloadable files (CSV, PDFs, exports)&lt;/li&gt;
&lt;li&gt;Transforming or aggregating data&lt;/li&gt;
&lt;li&gt;Proxying or forwarding requests&lt;/li&gt;
&lt;li&gt;Streaming responses from LLMs&lt;/li&gt;
&lt;li&gt;Handling third-party &lt;a href="https://docs.strapi.io/cms/backend-customization/webhooks" rel="noopener noreferrer"&gt;webhooks&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These patterns become increasingly powerful as your application grows and your API endpoints take on backend-for-frontend duties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;For this walkthrough, we start with a Next.js 16 dashboard application based on the current &lt;a href="https://nextjs.org/learn/dashboard-app" rel="noopener noreferrer"&gt;official App Router tutorial&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The project has been extended with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://proud-acoustics-2ed8c6b135.strapiapp.com/api" rel="noopener noreferrer"&gt;Strapi backend&lt;/a&gt; providing invoice and customer data&lt;/li&gt;
&lt;li&gt;A JSON REST API built entirely through App Router route handlers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Throughout the article, you'll build 3 advanced server-side capabilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CSV export route: Generate downloadable invoice data directly from a route handler.&lt;/li&gt;
&lt;li&gt;Locale-aware currency conversion route: Transform data using live exchange rates, plus a proxy that redirects based on the client’s IP.&lt;/li&gt;
&lt;li&gt;Redirect management system: A middleware powered by Vercel Edge Config and a dedicated route handler for efficient redirect lookups.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the end, you'll see how route handlers can cleanly encapsulate backend logic without needing a separate API service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;This guide assumes you’re comfortable working with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React’s &lt;code&gt;page.ts|js&lt;/code&gt; files in the Next.js 16 App Router&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://strapi.io/blog/what-is-a-rest-api-beginners-guide-examples-using-strapi" rel="noopener noreferrer"&gt;REST endpoints&lt;/a&gt; using standard HTTP verbs (GET, POST, PUT, DELETE).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Vercel Edge Config
&lt;/h3&gt;

&lt;p&gt;Two of the examples rely on &lt;strong&gt;Vercel Edge Config&lt;/strong&gt;, so it's helpful to be familiar with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vercel.com/docs/edge-config/get-started#create-an-edge-config-store" rel="noopener noreferrer"&gt;Create an Edge Config store&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/docs/edge-config/get-started#connect-your-vercel-project" rel="noopener noreferrer"&gt;Connect it to your Vercel project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vercel.com/docs/edge-config/get-started#pull-the-latest-environment-variables" rel="noopener noreferrer"&gt;Pull configuration values locally&lt;/a&gt; using &lt;code&gt;vercel env pull&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://vercel.com/docs/edge-config/edge-config-sdk" rel="noopener noreferrer"&gt;&lt;code&gt;@vercel/edge-config&lt;/code&gt;&lt;/a&gt; SDK for accessing config values in route handlers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  System / Environment Requirements
&lt;/h3&gt;

&lt;p&gt;To follow along after forking the starter repository, you’ll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The latest version of &lt;a href="https://nodejs.org/en" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm or pnpm&lt;/li&gt;
&lt;li&gt;A cloned or scaffolded version of the example app:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-next-app &lt;span class="nt"&gt;--example&lt;/span&gt; https://github.com/anewman15/nextjs-route-handlers/tree/prepare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Starter Code — A Typical JSON API Built with Next.js 16 Route Handlers
&lt;/h2&gt;

&lt;p&gt;The starter code for the example Next.js 16 app router API application is available in the &lt;a href="https://github.com/anewman15/nextjs-route-handlers" rel="noopener noreferrer"&gt;&lt;code&gt;prepare&lt;/code&gt; branch here&lt;/a&gt;. It has a similar-looking dashboard application with &lt;code&gt;customers&lt;/code&gt; and &lt;code&gt;invoices&lt;/code&gt; resources as the original Next.js tutorial. Some features have been discarded for brevity, focusing on the topics of this post. &lt;/p&gt;

&lt;p&gt;For demonstration purposes, the base &lt;code&gt;invoices&lt;/code&gt; REST endpoints are already implemented in this branch. You’ll expand on these throughout the article as you build more advanced functionality.&lt;/p&gt;

&lt;p&gt;To get the project running locally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bootstrap the starter project
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-next-app &lt;span class="nt"&gt;--example&lt;/span&gt; https://github.com/anewman15/nextjs-route-handlers/tree/prepare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;CD into the &lt;code&gt;app&lt;/code&gt; directory and install dependencies
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Start the development server:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Visit &lt;code&gt;http://localhost:3000/dashboard&lt;/code&gt;. You’ll see the classic Next.js dashboard with routes for:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/dashboard&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dashboard/invoices&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dashboard/customers&lt;/code&gt;&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Fqcsjt8cuyu96e0jjuy35.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.amazonaws.com%2Fuploads%2Farticles%2Fqcsjt8cuyu96e0jjuy35.png" alt="nextjs official dashboard.png" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This dashboard fetches resources from a Strapi backend hosted at:&lt;br&gt;
&lt;a href="https://proud-acoustics-2ed8c6b135.strapiapp.com/api" rel="noopener noreferrer"&gt;https://proud-acoustics-2ed8c6b135.strapiapp.com/api&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Because the data comes from a cloud-hosted CMS, you’ll need an active internet connection as you follow along.&lt;/p&gt;

&lt;p&gt;The examples in this article assume you are working from the &lt;code&gt;prepare&lt;/code&gt; branch.&lt;br&gt;
The fully completed project lives in the &lt;code&gt;main&lt;/code&gt; branch if you want to inspect the final implementation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Next.js Route Handlers for JSON REST APIs
&lt;/h2&gt;

&lt;p&gt;The starter code makes heavy use of route handlers (&lt;code&gt;route.ts&lt;/code&gt;) to implement its REST API. This aligns with the recommended pattern in the App Router: each nested segment can define its own API surface using exported HTTP method functions, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example: Collection Endpoint&lt;/strong&gt;: We will use a &lt;code&gt;GET()&lt;/code&gt; and a &lt;code&gt;POST()&lt;/code&gt; handler for &lt;code&gt;invoices&lt;/code&gt; endpoints at the &lt;code&gt;/api/v2/invoices/(locales)/en-us&lt;/code&gt; routing level:&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="c1"&gt;// Path: app/api/v2/invoices/(locales)/en-us/route.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/app/lib/strapi/strapiClient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allInvoices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allInvoices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createdInvoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createdInvoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;As it goes with React views in the &lt;code&gt;page.ts&lt;/code&gt; file, we can use dynamic route segments to define endpoints for an individual item (&lt;code&gt;invoices&lt;/code&gt; item in this case) with its &lt;code&gt;:id&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Single-Item Endpoint&lt;/strong&gt;: So, we have a route handler at &lt;code&gt;/api/v2/invoices/(locales)/en-us/[id]&lt;/code&gt;. For this segment, we have a &lt;code&gt;GET()&lt;/code&gt;, a &lt;code&gt;PUT()&lt;/code&gt; and a &lt;code&gt;DELETE()&lt;/code&gt; handler in the &lt;code&gt;route.ts&lt;/code&gt; file:&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="c1"&gt;// Path: app/api/v2/invoices/(locales)/en-us/[id]/route.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/app/lib/strapi/strapiClient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&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="o"&gt;&amp;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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PUT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&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="o"&gt;&amp;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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&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="o"&gt;&amp;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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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 Next.js 16 &lt;code&gt;route.ts&lt;/code&gt; file facilitates hosting multiple REST API endpoints by allowing more than one handler/action export. So, for each action for an &lt;code&gt;invoices&lt;/code&gt; item at &lt;code&gt;/api/v2/invoices/(locales)/en-us/[id]&lt;/code&gt;, we have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;GET()&lt;/code&gt; handler that serves an &lt;code&gt;invoices&lt;/code&gt; item with &lt;code&gt;:id&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;PUT()&lt;/code&gt; handler that helps update an &lt;code&gt;invoices&lt;/code&gt; item with &lt;code&gt;:id&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;DELETE()&lt;/code&gt; handler that helps delete an &lt;code&gt;invoices&lt;/code&gt; item with &lt;code&gt;:id&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;NextRequest&lt;/code&gt; and &lt;code&gt;NextResponse&lt;/code&gt; APIs in Next.js 16
&lt;/h2&gt;

&lt;p&gt;Route handlers use &lt;a href="https://nextjs.org/docs/13/pages/api-reference/functions/next-server" rel="noopener noreferrer"&gt;&lt;code&gt;NextRequest&lt;/code&gt; and &lt;code&gt;NextResponse&lt;/code&gt;&lt;/a&gt;, which extend the native Web APIs (Request and Response) with features optimized for the Next.js runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access to cookies and headers&lt;/li&gt;
&lt;li&gt;URL utilities&lt;/li&gt;
&lt;li&gt;Built-in JSON helpers&lt;/li&gt;
&lt;li&gt;Enhanced redirect and response handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These abstractions make building REST endpoints in the App Router ergonomic and familiar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication &amp;amp; Middleware in the Next.js App Router
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://strapi.io/blog/a-beginners-guide-to-authentication-and-authorization-in-strapi" rel="noopener noreferrer"&gt;Authentication&lt;/a&gt; can be introduced at multiple layers in a Next.js App Router application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sitewide auth — via OAuth, NextAuth.js, credentials, SSO, or JWT sessions&lt;/li&gt;
&lt;li&gt;Middleware-based authorization — using middleware.ts with matcher patterns for RBAC or LBAC&lt;/li&gt;
&lt;li&gt;Higher-order function (HOF) middleware — apply shared logic (auth, validation, logging) to groups of route handlers&lt;/li&gt;
&lt;li&gt;Service-specific SDKs — attach access tokens to requests for external services (e.g., Stripe, Strapi, Vercel APIs)&lt;/li&gt;
&lt;li&gt;Custom per-handler logic — manually adding headers inside individual route handlers, adding auth tokens to request headers from individual route handlers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example: Strapi Token Authentication
&lt;/h3&gt;

&lt;p&gt;In our case above, we are handling the Strapi token authentication very granularly at every request with the &lt;a href="https://docs.strapi.io/cms/api/client" rel="noopener noreferrer"&gt;&lt;code&gt;strapiClient&lt;/code&gt;&lt;/a&gt; initialized in &lt;code&gt;app/lib/strapi/strapiClient.ts&lt;/code&gt;:&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="c1"&gt;// Path: app/lib/strapi/strapiClient.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;strapi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@strapi/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;strapiClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;strapi&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_API_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_TOKEN_SALT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revenues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;strapiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;revenues&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;strapiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoices&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;strapiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customers&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;This &lt;code&gt;strapiClient&lt;/code&gt; sends the &lt;code&gt;auth&lt;/code&gt; token as a header at every Strapi request, so in this case, there is no need for authorizing backend Strapi requests with a specialized/shared middleware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js 16 Route Handlers - 3 Advanced Use Cases
&lt;/h2&gt;

&lt;p&gt;So far, we’ve used route handlers for relatively standard REST endpoints. But route handlers really shine when you start delegating heavier backend responsibilities to them.&lt;/p&gt;

&lt;p&gt;Because they run on the server and are built on top of the Web &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt; APIs, route handlers are a great fit for tasks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating and streaming files (CSV, PDF, etc.)&lt;/li&gt;
&lt;li&gt;Aggregating or transforming data from multiple sources&lt;/li&gt;
&lt;li&gt;Implementing location- or locale-aware APIs&lt;/li&gt;
&lt;li&gt;Handling webhooks and callback URLs&lt;/li&gt;
&lt;li&gt;Acting as a backend-for-frontend layer without a separate API service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is always &lt;strong&gt;cost vs performance&lt;/strong&gt;: you want to keep serverless invocations efficient while still doing useful work at the edge of your app.&lt;/p&gt;

&lt;p&gt;In the next sections, we’ll walk through three concrete, production-style use cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A CSV download route for invoices&lt;/li&gt;
&lt;li&gt;A location-based proxy + currency conversion endpoint&lt;/li&gt;
&lt;li&gt;A redirect-management middleware backed by Vercel Edge Config and a dedicated route handler&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Use Case 1: Implementing a File Download (CSV Export)
&lt;/h3&gt;

&lt;p&gt;A classic pattern for route handlers is file generation: fetch data, transform it, and stream it back as a downloadable file.&lt;/p&gt;

&lt;p&gt;In this example, we want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch all invoices from our Strapi backend&lt;/li&gt;
&lt;li&gt;Convert them to CSV&lt;/li&gt;
&lt;li&gt;Return the result as an &lt;code&gt;invoices.csv&lt;/code&gt; file from a route handler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ll create a route at &lt;code&gt;/api/v2/invoices/(locales)/en-us/csv&lt;/code&gt;:&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="c1"&gt;// Path : ./app/api/v1/invoices/(locales)/en-us/csv.route.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;json2csv&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json-2-csv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/app/lib/strapi/strapiClient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allInvoices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;populate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&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;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoicesCSV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;json2csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allInvoices&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;expandNestedObjects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoicesCSV&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Disposition&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attachment; filename="invoices.csv"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/csv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;headers&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;Here’s what happens step-by-step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;invoices.find()&lt;/code&gt; fetches invoice data (including related customer info) from Strapi.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/json2csv" rel="noopener noreferrer"&gt;&lt;code&gt;json2csv&lt;/code&gt; &lt;/a&gt;converts the JSON array into CSV.&lt;/li&gt;
&lt;li&gt;We create a &lt;code&gt;Buffer&lt;/code&gt; from the CSV string.&lt;/li&gt;
&lt;li&gt;We set headers so the browser treats the response as a file download.&lt;/li&gt;
&lt;li&gt;We return a raw &lt;code&gt;Response&lt;/code&gt; containing the CSV buffer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, any &lt;code&gt;GET&lt;/code&gt; request to &lt;code&gt;/api/v2/invoices/(locales)/en-us/csv&lt;/code&gt; returns a downloadable &lt;code&gt;invoices.csv&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;In the UI, we simply wire a “Download as CSV” button on the &lt;code&gt;/dashboard/invoices&lt;/code&gt; page to this endpoint:&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.amazonaws.com%2Fuploads%2Farticles%2Fsqpxmdohbjtaf4r0atbx.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.amazonaws.com%2Fuploads%2Farticles%2Fsqpxmdohbjtaf4r0atbx.png" alt="download csv.png" width="352" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This keeps file generation purely server-side, with a minimal surface in your React components.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Case 2: Location-Based Proxy + Data Transformation
&lt;/h3&gt;

&lt;p&gt;For the second use case, we’ll combine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A data transformation route handler that converts invoice amounts from USD → EUR from &lt;a href="https://open.er-api.com" rel="noopener noreferrer"&gt;Exchange Rates API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A location-aware proxy route handler that redirects traffic based on the client’s country&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of pattern is useful when you want to serve region-specific views of the same underlying data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Currency Conversion Endpoint (&lt;code&gt;/api/v2/invoices/(locales)/fr&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, we create a route handler that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetches invoices in USD using &lt;code&gt;invoices.find()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fetches live forex rates from the exchange rates provider&lt;/li&gt;
&lt;li&gt;Returns invoices with amounts converted to EUR using the &lt;code&gt;NextResponse&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Path: ./app/api/v2/invoices/(locales)/fr/route.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/app/lib/strapi/strapiClient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;invoicesInUSD&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;USDRates&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;invoicesInUSD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice_status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;populate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image_url&lt;/span&gt;&lt;span class="dl"&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;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;USDRates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://open.er-api.com/v6/latest/USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;EUR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;USDtoEURRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;USDRates&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.86&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoicesJSON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoicesInUSD&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoicesInEUR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoicesJSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;USDtoEURRate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;invoice_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;invoice_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;invoicesInEUR&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All the “heavy” work — external API call + transformation — happens server-side.&lt;/li&gt;
&lt;li&gt;The client just consumes a clean JSON API with EUR amounts.&lt;/li&gt;
&lt;li&gt;You can swap out the forex provider or rate logic without touching any React code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are able to access this directly from the &lt;code&gt;/api/v2/invoices/(locales)/fr&lt;/code&gt; endpoint. However, we want to implement a location-based proxy that redirects routing based on the request IP address. In particular, if the user is requesting &lt;code&gt;/api/v1/invoices&lt;/code&gt; from an IP located in FR, we want to redirect them to &lt;code&gt;/api/v2/invoices/(locales)/fr&lt;/code&gt;. Otherwise, we send the invoices as in v1.&lt;/p&gt;

&lt;p&gt;Let's now add a location proxy route handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Location-Based Proxy (/api/v1/invoices)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next, we create a location-aware proxy route:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the request originates from France (FR), we redirect to the &lt;code&gt;/fr&lt;/code&gt; endpoint.&lt;/li&gt;
&lt;li&gt;Otherwise, we return the default invoices as-is.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Path: app/api/v1/invoices/route.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/app/lib/strapi/strapiClient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-forwarded-for&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="c1"&gt;// const ip = "51.158.36.186"; // FR based ip&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;allInvoices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;allInvoices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://ip-api.com/json/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())?.&lt;/span&gt;&lt;span class="nx"&gt;countryCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FR&lt;/span&gt;&lt;span class="dl"&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;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allInvoices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/v2/invoices/fr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;302&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;Behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Non-French IPs hit &lt;code&gt;/api/v1/invoices&lt;/code&gt; and receive the default USD dataset.&lt;/li&gt;
&lt;li&gt;French IPs are transparently redirected to &lt;code&gt;/api/v2/invoices/fr&lt;/code&gt;, where amounts are converted to EUR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a perfect example of route handlers acting as a focused, backend-for-frontend layer: regional logic lives in the API, not in your React components.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Case 3: Redirect Management with Middleware + Vercel Edge Config
&lt;/h3&gt;

&lt;p&gt;For the final example, we’ll build a redirect management system that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stores redirect rules in Vercel Edge Config&lt;/li&gt;
&lt;li&gt;Uses a dedicated route handler to fetch individual redirect entries&lt;/li&gt;
&lt;li&gt;Uses a middleware (in Next.js 16, proxy.ts) to decide whether to redirect a request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern scales to thousands of redirects while keeping middleware fast and lightweight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Store Redirects in Edge Config&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the first step, we should add a redirects map to an Edge Config Store. Use &lt;a href="https://vercel.com/docs/edge-config/get-started" rel="noopener noreferrer"&gt;this Vercel Edge Config guide&lt;/a&gt; to add a redirects map to Vercel Edge Config.&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"-api-v1-revenues"&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;"destination"&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/v2/revenues/en-us"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permanent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;"-api-v1-revenues-fr"&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;"destination"&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/v2/revenues/en-us"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permanent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;"-api-v1-revenues-de"&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;"destination"&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/v2/revenues/en-us"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permanent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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="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;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keys represent normalized request paths (e.g., &lt;code&gt;/api/v1/revenues&lt;/code&gt; → &lt;code&gt;-api-v1-revenues&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Values contain a destination and a permanent flag.&lt;/li&gt;
&lt;li&gt;Edge Config keys can only contain alphanumeric characters, -, and _.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Follow Vercel’s docs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create the Edge Config store&lt;/li&gt;
&lt;li&gt;Connect it to your project&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;vercel env pull&lt;/code&gt; to bring environment variables into your local &lt;code&gt;env.local&lt;/code&gt; file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Route Handler for a Single Redirect Item&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next, we expose a route handler that returns a single redirect entry from Edge Config.&lt;/p&gt;

&lt;p&gt;This keeps the middleware simple; it doesn’t have to know how to talk to Edge Config directly.&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="c1"&gt;// Path: app/api/redirects/[path]&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vercel/edge-config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&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="o"&gt;&amp;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;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirectEntry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirectEntry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;What this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads the path parameter from the URL&lt;/li&gt;
&lt;li&gt;Uses get() from @vercel/edge-config to read the corresponding redirect entry&lt;/li&gt;
&lt;li&gt;Returns the entry as JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Middleware to Apply Redirects&lt;/strong&gt;&lt;br&gt;
Finally, we wire up a middleware that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Normalizes the incoming &lt;code&gt;pathname&lt;/code&gt; into a redirect key&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;edgeConfigHas()&lt;/code&gt; to quickly check if a redirect exists for that key&lt;/li&gt;
&lt;li&gt;If it exists, fetches the full redirect entry from the route handler&lt;/li&gt;
&lt;li&gt;Redirects with &lt;code&gt;307&lt;/code&gt; or &lt;code&gt;308&lt;/code&gt; depending on the &lt;code&gt;permanent&lt;/code&gt; flag&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Add a &lt;code&gt;proxy.ts&lt;/code&gt; (as opposed to &lt;code&gt;middleware.ts&lt;/code&gt;, which is in version 15) file and use the following code:&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="c1"&gt;// Path: proxy.ts&lt;/span&gt;

&lt;span class="cm"&gt;/* eslint-disable @typescript-eslint/no-explicit-any */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;has&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;edgeConfigHas&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vercel/edge-config&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;RedirectsEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;destination&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="nl"&gt;permanent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RedirectsRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RedirectsEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirectKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;edgeRedirectsHasRedirectKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;edgeConfigHas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirectKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edgeRedirectsHasRedirectKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirectApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`/api/redirects/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;redirectKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirectData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirectApi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;redirectEntry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RedirectsEntry&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redirectData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;redirectEntry&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;permanent&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;308&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;307&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirectEntry&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nx"&gt;statusCode&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/(api/v1/revenues*.*)&lt;/span&gt;&lt;span class="dl"&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 whole point of a Next.js root middleware is to keep its operations lightweight so that it can make faster routing decisions. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;So, first, without much overload, we just reached out to Vercel Edge Config with &lt;code&gt;has()&lt;/code&gt; to quickly decide whether the redirect map has a key that represents the requested URL &lt;code&gt;pathname&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;We have to do some string gymnastics in the meantime to conform to Edge Config rules for naming Edge Config keys with alphanumeric characters, &lt;code&gt;-&lt;/code&gt;, and &lt;code&gt;_&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the key doesn't exist, our decision is to swiftly continue to the next step of the request cycle with &lt;code&gt;NextResponse.next()&lt;/code&gt;. So, this improves routing decisions on the spot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the other hand, if the redirects map has an item for the &lt;code&gt;pathname&lt;/code&gt; being requested, we have additional things to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a URL from the &lt;code&gt;pathname&lt;/code&gt; that represents a redirect URL key.&lt;/li&gt;
&lt;li&gt;Send a &lt;code&gt;fetch()&lt;/code&gt; request to the redirects key endpoint we created above at &lt;code&gt;/api/redirects/[path]&lt;/code&gt; for this redirect URL key. Get that redirect entry via the route handler dedicated to this URL key.&lt;/li&gt;
&lt;li&gt;redirect the request to the &lt;code&gt;redirectEntry.destination&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, here, we could have sent a &lt;code&gt;get()&lt;/code&gt; request in the first place to Edge Config, but that would be a lengthy thing for the middleware to tackle -- particularly costly for a request that does not have a redirect URL. This becomes obvious when you have more and more entries in your redirects map. &lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;has()&lt;/code&gt; is quicker and we have a route handler to tackle lengthy data fetching, we are choosing to keep our middleware performant. Essentially, for an efficient specialty middleware, we have handed off the otherwise slower yielding task of data fetching over to a route handler, which does it well in itself under the hood.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Advanced Route Handler Use Cases
&lt;/h3&gt;

&lt;p&gt;Beyond the 3 examples above, Next.js 16 route handlers are a great fit for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implementing draft mode with Next.js 16 &lt;code&gt;draftMode&lt;/code&gt; APIs, where third-party CMS data is accessed via a route handler.&lt;/li&gt;
&lt;li&gt;Features A/B testing, where feature-related live user data is fetched through a route handler.&lt;/li&gt;
&lt;li&gt;Handling webhooks from third-party services like payment gateways, instrumentation tools, CMS content providers, and the like.&lt;/li&gt;
&lt;li&gt;Redirecting requests to a callback URL sent by third-party service workflows.&lt;/li&gt;
&lt;li&gt;Implementing mailers from fetched/aggregated data.&lt;/li&gt;
&lt;li&gt;Streaming responses from LLM models.&lt;/li&gt;
&lt;li&gt;Handling server-sent events via the SSE protocol.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next.js 16 Route Handlers - Pitfalls &amp;amp; Best Practices
&lt;/h2&gt;

&lt;p&gt;While route handlers bring a powerful backend-for-frontend model to the App Router, they also introduce important architectural considerations. To use them effectively, especially for non-trivial backend logic, you need to be aware of their runtime constraints and apply performance-friendly patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next.js Route Handler Limitations
&lt;/h3&gt;

&lt;p&gt;Because Next.js deploys route handlers as serverless functions (Node or Edge runtimes), they come with inherent limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://strapi.io/blog/real-time-chat-application-using-strapi-next-socket-io-and-postgre-sql" rel="noopener noreferrer"&gt;WebSockets&lt;/a&gt; cannot be used reliably due to connection timeouts.&lt;/li&gt;
&lt;li&gt;Shared state between requests is not guaranteed; execution is isolated and ephemeral.&lt;/li&gt;
&lt;li&gt;File-system writes may not work in serverless environments.&lt;/li&gt;
&lt;li&gt;Lengthy or blocking operations may be terminated due to execution time limits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These constraints should guide how much work you push into a route handler and when to offload tasks elsewhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced Next.js 16 Route Handlers Best Practices
&lt;/h3&gt;

&lt;p&gt;Just like other aspects of Next.js 16, route handler best practices obviously involve making proper trade-offs between cost and performance. &lt;/p&gt;

&lt;p&gt;Since Next.js route handlers are serverless and subject to timeouts, we need to make sure route handlers are implemented in such a way that optimizes persistent performance against the cost of the processes handled at the route. &lt;/p&gt;

&lt;p&gt;To get consistent performance and predictable behavior, keep the following best practices in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimize long-running data transformations&lt;/strong&gt;. Push heavy workloads to background jobs or external services whenever possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce the number of aggregated data sources&lt;/strong&gt;. If you’re combining more than a few external APIs, consider a dedicated aggregator service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use granular, specialized route handlers&lt;/strong&gt;. Smaller handlers are easier to cache and revalidate using Next.js dynamic and revalidate configs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply scoped access control&lt;/strong&gt;. Use middleware matchers, location-based or role-based strategies, or shared higher-order middleware to keep endpoints secure and performant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leverage shared middleware&lt;/strong&gt; for context forwarding. Keep route handlers focused and push repeated logic, like auth, logging, or header injection, into shared HOF middleware where possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this post, we explored how Next.js 16 route handlers can power both standard and advanced server-side features within an App Router application. We began by reviewing how route handlers implement RESTful JSON APIs and then added a CSV file download endpoint built entirely in a route handler.&lt;/p&gt;

&lt;p&gt;From there, we demonstrated how route handlers can take on more complex backend tasks, including a currency-conversion API backed by a location-aware proxy layer. We also implemented a redirect-management middleware using Vercel Edge Config and saw how a dedicated route handler improves performance by handling redirect lookups outside the middleware.&lt;/p&gt;

&lt;p&gt;Finally, we touched on additional advanced use cases, along with the limitations and best practices to keep in mind when using route handlers for production workloads.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dev.tourl"&gt;strapi.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>apigateway</category>
      <category>routing</category>
    </item>
    <item>
      <title>Strapi 5 Migration: Why It Is Essential for Modern CMS</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 06 Feb 2026 10:48:53 +0000</pubDate>
      <link>https://dev.to/strapi/strapi-5-migration-why-it-is-essential-for-modern-cms-1f92</link>
      <guid>https://dev.to/strapi/strapi-5-migration-why-it-is-essential-for-modern-cms-1f92</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;We are living in a dynamic digital-only era where a content management system (CMS) is much more than just a publishing tool. In 2025 and beyond, it will serve as the core engine powering omnichannel experiences, customer engagement, and organizational agility. &lt;/p&gt;

&lt;p&gt;To align with these demands, Strapi released its Strapi 5 update that has redefined how enterprises can model, deliver, and scale their digital content strategies. &lt;/p&gt;

&lt;p&gt;Enterprises that are still relying on Strapi v3 or v4 are beginning to feel the impact of performance bottlenecks, limited API flexibility, and declining support options. This is why migrating to Strapi 5 is no longer just a migration option for businesses; it’s the right way forward.&lt;/p&gt;

&lt;p&gt;This blog will explore the significant need to upgrade to Strapi 5 as an enterprise, what it can benefit you with, and what organizations can do to ensure a smooth transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Staying on Strapi v3 or v4 Holds You Back?
&lt;/h2&gt;

&lt;p&gt;Staying with the older versions of Strapi can seem like a safer option from an enterprise mindset, but that comes with associated technical debt. &lt;/p&gt;

&lt;p&gt;As Strapi continues to update, support for old versions will gradually lapse, and businesses will face difficulties fixing bugs &amp;amp; adding security updates, as well as new features that are available to them on the latest versions automatically. &lt;/p&gt;

&lt;p&gt;One other area that can be impacted is performance. Even on versions 3 and 4, some businesses with higher visitor volumes experience latency issues. &lt;/p&gt;

&lt;p&gt;Moreover, new tools, APIs, and technologies, such as &lt;a href="https://strapi.io/ai" rel="noopener noreferrer"&gt;Strapi AI&lt;/a&gt;, are being developed with &lt;a href="https://strapi.io/five" rel="noopener noreferrer"&gt;Strapi 5&lt;/a&gt; compatibility as the baseline, making it harder for legacy versions to keep pace. These challenges not only slow down business operations but also result in severe financial losses for the business.&lt;/p&gt;

&lt;p&gt;Let’s understand this further with an example of API performance difference in both versions:&lt;/p&gt;

&lt;p&gt;In Strapi v4, fetching multiple content types often required nested queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;strapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;populate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;category&lt;/span&gt;&lt;span class="dl"&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;However, in Strapi 5, this is optimized with the new &lt;a href="https://docs-v4.strapi.io/dev-docs/api/query-engine" rel="noopener noreferrer"&gt;Query Engine API&lt;/a&gt; for faster population and cleaner syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;strapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;populate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;This not only simplifies code readability but also improves query speed by up to 30% in production environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Advantages of Strapi 5 for Enterprises
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fxi4ddpjmpe9y27qclqxg.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.amazonaws.com%2Fuploads%2Farticles%2Fxi4ddpjmpe9y27qclqxg.png" alt="Advantages of Strapi 5" width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Migrating to Strapi 5 offers multiple advantages for enterprise-grade transformation. In this section, we will briefly explore these benefits. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Future-Ready Architecture&lt;/strong&gt;&lt;br&gt;
Strapi 5 possesses a more modular and extensible architecture, powered by Strapi AI at the core. This means enterprises can update their tech stack seamlessly without restrictions, deliver smarter content experiences, and have better integration flexibility for newly launched tools and future upgrades.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Improved Content Modeling&lt;/strong&gt;&lt;br&gt;
One of the standout capabilities of Strapi 5 is the new &lt;a href="https://docs.strapi.io/cms/features/content-type-builder" rel="noopener noreferrer"&gt;content type builder&lt;/a&gt;. This version enables faster and simpler modeling through Strapi AI, which reduces the complexity of centralizing multiple content types across different platforms. With minimal complexities, your teams spend less time on configuration and more on creating &amp;amp; publishing content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Improved Performance and API Responding&lt;/strong&gt;&lt;br&gt;
APIs are the lifeline of any headless CMS. This is why Strapi 5 is designed to provide an enhanced API experience, meaning you can expect faster API calls and reduced latency. They offer tangible benefits for enterprises with global audiences - you'll notice improved response times and enhanced content delivery experiences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Improved Developer Experience&lt;/strong&gt;&lt;br&gt;
Another benefit of Strapi 5 is that it provides developers with a &lt;a href="https://strapi.io/blog/shipping-faster-with-strapi-ai-translations-and-more-homepage-customizations" rel="noopener noreferrer"&gt;better experience&lt;/a&gt;, as it comes with enhanced tools/features, including 100% TypeScript, a new API response format, a launchpad demo app, enhanced content versioning, history, and draft management, and more. This helps improve their productivity and reduce deployment timelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Improved Security Measures&lt;/strong&gt;&lt;br&gt;
The new version also features enhanced security capabilities. These features include enhanced encryption protocols, improved options for user authentication, and regular vulnerability assessments to help safeguard your critical data. These features help ensure compliance with industry standards and safety from rising cyber threats for both the CMS and your integrated system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. New Plugins Ecosystem&lt;/strong&gt;&lt;br&gt;
Lastly, Strapi 5 covers a more enhanced plugin ecosystem, including &lt;a href="https://docs.strapi.io/cms/plugins-development/plugin-sdk" rel="noopener noreferrer"&gt;Plugin SDK&lt;/a&gt; and a dedicated Plugin CLI. This allows the developers of a professional Strapi development company to either build custom plugins or integrate &lt;a href="https://market.strapi.io/plugins" rel="noopener noreferrer"&gt;community-built plugins&lt;/a&gt; more seamlessly, thereby extending Strapi’s functionality as per your use case without disrupting the entire workflow.&lt;/p&gt;
&lt;h2&gt;
  
  
  Steps of Migration from Older Versions to Strapi 5
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fukqqizhxo9bgt69dmche.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.amazonaws.com%2Fuploads%2Farticles%2Fukqqizhxo9bgt69dmche.png" alt="Migration to Strapi 5 Steps" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Migrating to Strapi 5 requires planning but offers substantial long-term returns. The step-by-step migration involves:&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Preparing the environment
&lt;/h3&gt;

&lt;p&gt;The first step of the Strapi 5 migration is to &lt;a href="https://docs.strapi.io/cms/features/data-management" rel="noopener noreferrer"&gt;back up&lt;/a&gt; all your Strapi assets, including your database, uploads, and configuration files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Backup MongoDB or PostgreSQL data&lt;/span&gt;
pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; &amp;lt;db_user&amp;gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &amp;lt;db_host&amp;gt; &lt;span class="nt"&gt;-Fc&lt;/span&gt; &amp;lt;db_name&amp;gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup.dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, ensure that your infrastructure is compatible, and you’re using Node.js 18 nd npm v9+ to proceed with migration, as Strapi no longer supports older versions.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Migrating content models and APIs
&lt;/h3&gt;

&lt;p&gt;Once your data is backed up and the installation is completed, the next step is to migrate the content models and APIs. The best part of this version is that it introduces a more declarative content-type schema. &lt;/p&gt;

&lt;p&gt;If you’re coming from Strapi v4, you’ll notice configuration paths and naming conventions have changed.&lt;/p&gt;

&lt;p&gt;When &lt;a href="https://docs.strapi.io/cms/migration/v4-to-v5/step-by-step" rel="noopener noreferrer"&gt;migrating from Strapi v4 to Strapi 5&lt;/a&gt;, the model schema is automatically transitioned with &lt;code&gt;npx @strapi/upgrade major&lt;/code&gt;. This eases the content model migration process. &lt;/p&gt;

&lt;p&gt;However, remember that custom codes, controllers, services, and policies still require manual updates. For more information, you can refer to the &lt;a href="https://docs.strapi.io/cms/migration/v4-to-v5/step-by-step#step-2-run-automated-migrations" rel="noopener noreferrer"&gt;Strapi 5 migration documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, let’s understand Strapi v4 → Strapi 5 migration for content type structure with an example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strapi v4 Structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/src/api/article/models/article.settings.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Strapi 5 Structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/src/api/article/content-types/schema.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Updating Custom Plugins and Middlewares
&lt;/h3&gt;

&lt;p&gt;Custom plugins designed in Strapi v3/v4 may depend on deprecated APIs and require changes based on the new architecture. Hence, updating them becomes essential for plugin lifecycle consistency. &lt;/p&gt;

&lt;p&gt;With the new plugin SDK introduced in Strapi 5, creating custom plugins becomes seamless as it allows modular development and reduces complexity.&lt;br&gt;
Here’s how you can update plugin architecture to new APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Strapi v4 plugin export&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;strapi&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;span class="c1"&gt;// Strapi 5 plugin export&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;strapi&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nf"&gt;bootstrap&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;h3&gt;
  
  
  Step 4: Testing and validation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.strapi.io/cms/testing" rel="noopener noreferrer"&gt;Testing&lt;/a&gt; is essential to ensure application performance, security, and API responses meet enterprise standards. Once the plugin update and content model migration are completed, begin the testing phase. &lt;/p&gt;

&lt;p&gt;Run automated test suites to validate API responses, permissions, and content delivery.&lt;/p&gt;

&lt;p&gt;To explain with an example, you can test API response validation (using Jest):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET /api/article returns valid response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;strapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpServer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/articles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeDefined&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;Also, make sure to use Strapi’s built-in &lt;code&gt;--watch-admin&lt;/code&gt; flag to rebuild the admin panel dynamically during testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run develop &lt;span class="nt"&gt;--watch-admin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Deploy on Strapi Cloud or Self-managed Cloud Setup
&lt;/h3&gt;

&lt;p&gt;Once all steps have been completed and the testing is done, you can push the changes to your CI/CD pipeline &amp;amp; deploy it on Strapi Cloud or any other third-party compatible setup. &lt;/p&gt;

&lt;p&gt;Using Strapi Cloud is suggested to improve functionality and compatibility with the provided features and tools of Strapi 5. &lt;/p&gt;

&lt;p&gt;For example, for Strapi 5 deployment with GitHub, run this command-&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.amazonaws.com%2Fuploads%2Farticles%2Fvoqeb68zttv6pvd42mz5.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.amazonaws.com%2Fuploads%2Farticles%2Fvoqeb68zttv6pvd42mz5.png" alt="Deploy Strapi 5 with GitHub" width="762" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, enterprises can cut migration complexity, reduce downtime, and optimize Strapi 5 adoption by working with an experienced &lt;a href="https://successive.tech/strapi-website-development/" rel="noopener noreferrer"&gt;Strapi development company&lt;/a&gt;. The technical expertise helps with faster deployment and time-to-market, without disrupting the existing operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Significant Strapi Feature Updates You Should Know
&lt;/h2&gt;

&lt;p&gt;The Strapi 5 update is significantly focused on enterprise-grade innovation, and to enable that, it has released two major feature updates: Strapi AI and enhancements to Strapi Cloud. &lt;/p&gt;

&lt;p&gt;For organizations planning a Strapi 5 migration, these advancements ensure a modern, efficient, and future-proof CMS foundation.&lt;/p&gt;

&lt;p&gt;Let’s quickly understand these updates, their significance in Strapi 5, and how they can support your unique business requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strapi AI
&lt;/h3&gt;

&lt;p&gt;The newly launched Strapi AI brings generative AI capabilities directly into the CMS interface, enabling businesses to build intelligent content workflows. This feature helps teams accelerate content creation with a smart prompting mechanism, automate editorial suggestions, and improve SEO readiness with context-aware recommendations. It can summarize, rephrase, or localize content without leaving the CMS dashboard and can seamlessly integrate with your existing content approval flows. &lt;/p&gt;

&lt;p&gt;Additionally, you can generate entire content schemas via AI chatbots either by running a prompt or by uploading a Figma file / an existing frontend app. The infusion of Strapi AI reduces manual efforts and helps you maintain consistency across channels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strapi Cloud Enhancements
&lt;/h3&gt;

&lt;p&gt;Strapi Cloud has evolved to simplify deployment and scaling for global enterprises.&lt;br&gt;
 With upgraded CI/CD integrations, automated scaling across multiple environments, and built-in monitoring tools, it now delivers a truly enterprise-ready platform for seamless application development, testing, and deployment.&lt;br&gt;
The latest enhancements include one-click Strapi version upgrades, strengthened security and authentication, advanced caching, and a high-availability infrastructure designed to ensure fast, reliable global content delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Does Successive Digital Simplify Strapi 5 Migration?
&lt;/h2&gt;

&lt;p&gt;With over a decade of experience in CMS development &amp;amp; modernization, &lt;a href="https://successive.tech/" rel="noopener noreferrer"&gt;Successive Digital&lt;/a&gt; doesn’t just migrate your content management engine; they can transform your entire business ecosystem for a 360-degree transformation experience. &lt;/p&gt;

&lt;p&gt;To be precise, in the context of Strapi 5 migration, they seamlessly migrate content models, APIs, and custom plugins with zero data loss, re-architect your stack to maximize Strapi 5 scalability and flexibility, optimize API performance and system security, and lastly, conduct rigorous testing to guarantee zero downtime for your ongoing operations.&lt;/p&gt;

&lt;p&gt;Therefore, whether you’re on Strapi v3 or v4, their proven frameworks ensure a smooth and cost-effective upgrade to Strapi 5.&lt;br&gt;
Here are a few project glimpses that demonstrate their capabilities:&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-World Impact of Strapi 5 Migration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Yatra&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://strapi.io/user-stories/yatra-scaled-10m-users-4x-faster-campaigns-with-strapi" rel="noopener noreferrer"&gt;Successive assisted Yatra&lt;/a&gt; in modernizing its systems by moving its legacy CMS to Strapi. This led to a 40% reduction in costs and a 20% increase in user engagement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MyStays&lt;/strong&gt; &lt;br&gt;
Successive Digital migrated MyStays’ legacy system to Strapi, which streamlined booking by 25% and reduced time spent on content management by 40%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;In a digital economy where customer experience is paramount, using outdated versions of a CMS is a significant risk. This is the reason why Strapi 5 migration is recommended for enterprises expecting optimal operational continuity, rather than experiencing performance bottlenecks.&lt;/p&gt;

&lt;p&gt;This new version serves as an enabler of agility, scalability, and AI-driven innovation for these established brands, allowing them to deliver an enhanced digital experience.&lt;/p&gt;

&lt;p&gt;Furthermore, working with experts in developing Strapi-based applications, like &lt;a href="https://successive.tech/" rel="noopener noreferrer"&gt;Successive Digital&lt;/a&gt;, will help ensure your migration from Strapi v3 or Strapi v4 to Strapi 5 is seamless and is strategically aligned with your business goals.&lt;/p&gt;

&lt;p&gt;In a nutshell, Strapi 5 is the present and the future of content management and digital experience ecosystem. Hence, to stay ahead and competitive, enterprises must plan a migration.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://strapi.io/blog/strapi-5-migration-why-it-is-essential-for-modern-cms" rel="noopener noreferrer"&gt;strapi.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>strapiv5</category>
      <category>strapi5</category>
      <category>strapi</category>
      <category>cms</category>
    </item>
    <item>
      <title>How to Run A/B Tests with Strapi to Boost Conversions</title>
      <dc:creator>Theodore Kelechukwu Onyejiaku</dc:creator>
      <pubDate>Fri, 06 Feb 2026 10:44:41 +0000</pubDate>
      <link>https://dev.to/strapi/how-to-run-ab-tests-with-strapi-to-boost-conversions-jff</link>
      <guid>https://dev.to/strapi/how-to-run-ab-tests-with-strapi-to-boost-conversions-jff</guid>
      <description>&lt;p&gt;Experimentation is one of the most effective ways to make better product decisions. Among the most widely used methods is A/B testing, a technique that helps teams validate ideas with real user data before rolling out changes broadly. If your website is powered by Strapi, combining it with a headless A/B testing platform enables you to launch experiments and &lt;a href="https://strapi.io/blog/what-is-conversion-rate-optimization" rel="noopener noreferrer"&gt;optimize conversion rates&lt;/a&gt; without friction.&lt;/p&gt;

&lt;p&gt;In this post, we’ll cover the essentials of AB testing, best practices, common pitfalls, and how you can set up experimentation with Strapi and Croct.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is A/B testing?
&lt;/h2&gt;

&lt;p&gt;At its core, &lt;a href="https://blog.croct.com/post/taking-advantage-of-ab-testing" rel="noopener noreferrer"&gt;A/B testing&lt;/a&gt; is the process of comparing two or more versions of a page, component, or strategy to determine which one performs better in achieving a predefined goal.&lt;/p&gt;

&lt;p&gt;For example, you might test different headlines on your landing page to determine which drives more sign-ups. By randomly exposing a percentage of traffic to each version, you collect data that enables you to measure the impact of changes before making a full commitment.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should you run A/B tests?
&lt;/h3&gt;

&lt;p&gt;A/B testing is most valuable when you’re unsure about how changes in your interface or content could impact user behavior, revenue, or engagement. For instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploying a &lt;a href="https://strapi.io/blog/ab-testing-website-redesigns-developer-guide" rel="noopener noreferrer"&gt;website redesign&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Testing alternative &lt;a href="https://strapi.io/blog/custom-cta-components-that-convert" rel="noopener noreferrer"&gt;CTAs&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Validating new navigation layouts
&lt;/li&gt;
&lt;li&gt;Experimenting with pricing pages and product cards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond these examples, here are some situations where running an A/B test can make a real difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Early-stage features:&lt;/strong&gt; When you’re introducing a new product feature and want to validate if users actually engage with it as expected.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content optimization:&lt;/strong&gt; When you’re unsure which copy, design, or visual assets resonate best with your audience (here you can think about running &lt;a href="https://blog.croct.com/post/ab-tests-personalization" rel="noopener noreferrer"&gt;segmented AB tests&lt;/a&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversion funnels:&lt;/strong&gt; When drop-offs happen in sign-up or checkout flows, and you want to identify which adjustments reduce friction.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention strategies:&lt;/strong&gt; When you’re experimenting with onboarding experiences, emails, or in-app prompts to improve long-term user engagement.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seasonal campaigns:&lt;/strong&gt; When you want to compare variations of promotions or offers during high-traffic periods.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, you should consider running A/B tests whenever the outcome of a decision carries uncertainty and the potential impact on your business justifies the effort.&lt;/p&gt;

&lt;p&gt;A/B testing isn’t about testing everything. It’s about testing what matters most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why most companies struggle with AB testing
&lt;/h2&gt;

&lt;p&gt;While the concept of A/B testing sounds straightforward, many companies &lt;a href="https://blog.croct.com/post/product-ui-experimentation" rel="noopener noreferrer"&gt;struggle to make it work&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Many products aren’t built with experimentation in mind, so every test feels like a bespoke engineering project, especially if the architecture is rigid. This usually means that CRO teams often depend heavily on scarce developer resources, which slows down the iteration process. If you work with product interfaces or &lt;a href="https://strapi.io/blog/boost-ecommerce-conversion-rates" rel="noopener noreferrer"&gt;e-commerce stores&lt;/a&gt;, consider that applications are usually dynamic, stateful, and connected to backend systems, making experiments more challenging than simple marketing page tests.&lt;/p&gt;

&lt;p&gt;On the workflow and ownership side, we must account for the fact that product, growth, and engineering teams may not align on who drives experimentation. With unclear ownership, experimentation projects lead to stalled initiatives.&lt;/p&gt;

&lt;p&gt;As if this is not enough, let's consider the technical aspect of AB testing: when improperly implemented, experiments can compromise reliability and performance, which introduces risk and analytics hurdles. If you don't have the right tooling at your side, you'll fall short.&lt;/p&gt;

&lt;p&gt;In short, experimentation is challenging because of technical complexity, organizational misalignment, and a lack of purpose-built tools. Overcoming these barriers is key to unlocking continuous, meaningful conversion optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  A/B testing best practices
&lt;/h2&gt;

&lt;p&gt;Running effective experiments requires more than just changing button colors. The A/B test itself is just one of many parts of a long workflow, and you should pay attention to each step with the same care as the test itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. How to talk your boss into A/B testing
&lt;/h3&gt;

&lt;p&gt;Sometimes, the biggest challenge isn’t technical, it’s organizational. Convincing leadership to invest in experimentation requires framing it as a lever for growth, highlighting the ROI of A/B testing, and positioning it as a competitive advantage. &lt;a href="https://blog.croct.com/post/talk-your-boss-ab-testing" rel="noopener noreferrer"&gt;This guide&lt;/a&gt; provides practical guidance on framing the conversation, emphasizing risk reduction and continuous learning.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. How to prioritize your AB testing hypotheses
&lt;/h3&gt;

&lt;p&gt;Before even listing them, you should define what success means to you and which metrics you can use to validate it.&lt;/p&gt;

&lt;p&gt;Listing testing hypotheses requires both data knowledge and creativity, and there's nothing better than brainstorming.  You should invite all your team members to this assignment and have them consider what can improve the conversion rates. The more diverse this team is, the better.&lt;/p&gt;

&lt;p&gt;Once you have a list of ideas to test, use a method like the &lt;a href="https://blog.croct.com/post/ice-score-priorization" rel="noopener noreferrer"&gt;ICE scoring&lt;/a&gt; to evaluate each option by assessing its impact, your confidence in its potential outcome, and the ease of implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. How to plan your AB testing
&lt;/h3&gt;

&lt;p&gt;This should be the easiest step.&lt;/p&gt;

&lt;p&gt;Start by defining your primary and complementary metrics. Gather current traffic and conversion rate information to estimate the experiment duration beforehand (&lt;a href="https://docs.google.com/spreadsheets/d/1MaRR0jJcteDWnoAvsjpSq02oc3nFTv9UMJPVluZoo6M/edit" rel="noopener noreferrer"&gt;this calculator&lt;/a&gt; can help you). Then, define which method you will use to analyze the results.&lt;/p&gt;

&lt;p&gt;If you have a tight calendar, running multiple tests simultaneously may sound tempting. &lt;strong&gt;Don't do that&lt;/strong&gt;. Overlapping experiments could interfere with one another, so our advice is to keep it simple and test one hypothesis at a time.&lt;/p&gt;

&lt;p&gt;Finally, ensure that you include a step for documenting and sharing the learnings. Even failed experiments produce valuable insights. Maintain a knowledge base to prevent teams from repeating the same tests or mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to analyze your experiments
&lt;/h2&gt;

&lt;p&gt;Once the data comes in, it’s crucial to analyze the results correctly. Two common approaches are frequentist and Bayesian statistics, and we could write a whole article about each of them.&lt;/p&gt;

&lt;p&gt;While frequentist methods have been the standard for decades, Bayesian methods are increasingly popular due to their flexibility, intuitive insights, and faster conclusions.&lt;/p&gt;

&lt;p&gt;If you want to dive deeper into each of these topics, we suggest these two guides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.croct.com/post/bayesian-vs-frequentist" rel="noopener noreferrer"&gt;Bayesian or frequentist: which approach is better for AB testing?&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.croct.com/post/bayesian-ab-testing" rel="noopener noreferrer"&gt;Bayesian approach: a deep overview for AB testing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which tools can you use?
&lt;/h2&gt;

&lt;p&gt;The right tool can make or break your experimentation workflow. Choosing the right one depends on your team’s technical expertise and needs. It's not an easy task.&lt;/p&gt;

&lt;p&gt;Before discussing names, consider the four types of tools and the level of development dependence each one brings.&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.amazonaws.com%2Fuploads%2Farticles%2Fw94tuw1uyds7ozgf1lxs.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.amazonaws.com%2Fuploads%2Farticles%2Fw94tuw1uyds7ozgf1lxs.png" alt="AB testing tools.png" width="800" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. No-code and low-code tools
&lt;/h3&gt;

&lt;p&gt;Tools like VWO and Convert allow marketers and product managers to create experiments without any developer involvement.&lt;/p&gt;

&lt;p&gt;These tools typically provide a visual editor for modifying elements on a webpage and tracking conversions, but offer limited to no flexibility for complex experiments, and usually &lt;a href="https://blog.croct.com/pt-br/post/editor-visual-cro-perfomance" rel="noopener noreferrer"&gt;impact page performance&lt;/a&gt; due to client-side execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Headless experimentation platforms
&lt;/h3&gt;

&lt;p&gt;Platforms like &lt;a href="https://croct.com" rel="noopener noreferrer"&gt;Croct&lt;/a&gt; and Optimizely provide AB testing and &lt;a href="https://strapi.io/blog/how-to-apply-content-personalization-to-boost-conversion-rates" rel="noopener noreferrer"&gt;personalization&lt;/a&gt; directly within the content management system.&lt;/p&gt;

&lt;p&gt;These tools integrate seamlessly with dynamic websites, enabling teams to manage experiments without incurring engineering overhead, flickering, or performance issues. However, it requires a developer for the first integration and doesn't provide full flexibility for marketers in terms of design and layout.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. JavaScript-based tools
&lt;/h3&gt;

&lt;p&gt;JavaScript-based tools, such as AB Tasty, Amplitude Experiments, and Dynamic Yield, require a basic knowledge of JavaScript but offer more control over experiments. They typically involve embedding a script into the website to dynamically modify elements, which increases the dependence on developers for technical support at each test.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Feature flag
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://blog.croct.com/post/feature-flags" rel="noopener noreferrer"&gt;Feature flag tools&lt;/a&gt; like LaunchDarkly and Split.io allow teams to deploy new features to a subset of users without affecting everyone. Developers widely use them to roll out and test changes gradually, making them useful for backend-controlled experimentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Fully developer-driven
&lt;/h3&gt;

&lt;p&gt;These solutions offer the highest level of flexibility and control, enabling engineers to implement and manage A/B testing directly within the codebase. They’re often combined with analytics platforms for result tracking.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run AB tests on Strapi-powered websites
&lt;/h2&gt;

&lt;p&gt;There are many A/B testing tools on the market, but if you’re working with Strapi, &lt;a href="https://croct.com" rel="noopener noreferrer"&gt;Croct&lt;/a&gt; provides an out-of-the-box solution for experimentation and personalization. It stands out by offering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Component-level experimentation, ideal for modern frontend frameworks
&lt;/li&gt;
&lt;li&gt;Real-time updates without redeploying code
&lt;/li&gt;
&lt;li&gt;Advanced user segmentation and personalization capabilities
&lt;/li&gt;
&lt;li&gt;Easy API-first integration with Strapi&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://blog.croct.com/post/strapi-cms-ab-testing-personalization" rel="noopener noreferrer"&gt;This Strapi's guide&lt;/a&gt; walks you through adding experimentation on top of Strapi content, without complex development cycles.&lt;/p&gt;

&lt;p&gt;Additionally, Croct provides &lt;a href="https://strapi.io/integrations/croct" rel="noopener noreferrer"&gt;Strapi CMS integration templates&lt;/a&gt; to help you get started quickly with minimal setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A/B testing is a crucial tool for developing products and websites with higher conversion rates. By combining Strapi with Croct, you can empower your team to experiment faster, personalize content at scale, and base decisions on data rather than guesswork.&lt;/p&gt;

&lt;p&gt;If you’re ready to start running A/B tests and personalizing your Strapi-powered website, Croct is the easiest way to get there. Start planning your experiments today and see how it can take your website to the next level.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://strapi.io/blog/how-to-run-ab-tests-with-strapi-to-boost-conversions" rel="noopener noreferrer"&gt;strapi.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>strapi</category>
      <category>testing</category>
      <category>croct</category>
    </item>
  </channel>
</rss>
