惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security Affairs
N
News and Events Feed by Topic
T
Tenable Blog
P
Proofpoint News Feed
W
WeLiveSecurity
Simon Willison's Weblog
Simon Willison's Weblog
Google DeepMind News
Google DeepMind News
C
CERT Recently Published Vulnerability Notes
Help Net Security
Help Net Security
I
Intezer
T
Threat Research - Cisco Blogs
S
Secure Thoughts
C
Cyber Attacks, Cyber Crime and Cyber Security
L
Lohrmann on Cybersecurity
AWS News Blog
AWS News Blog
Google Online Security Blog
Google Online Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Know Your Adversary
Know Your Adversary
Project Zero
Project Zero
The Hacker News
The Hacker News
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Tor Project blog
N
News | PayPal Newsroom
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Hacker News - Newest:
Hacker News - Newest: "LLM"
A
Arctic Wolf
Forbes - Security
Forbes - Security
O
OpenAI News
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Security Latest
Security Latest
P
Palo Alto Networks Blog
S
Schneier on Security
S
Securelist
C
Cybersecurity and Infrastructure Security Agency CISA
H
Heimdal Security Blog
V
Vulnerabilities – Threatpost
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园_首页
T
Troy Hunt's Blog
Latest news
Latest news
Recent Announcements
Recent Announcements
MyScale Blog
MyScale Blog
人人都是产品经理
人人都是产品经理
L
LINUX DO - 热门话题
M
MIT News - Artificial intelligence
N
Netflix TechBlog - Medium
V
Visual Studio Blog
H
Hacker News: Front Page

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Build a Type-Safe Next.js 15 + Strapi v5 Blog in 10 Minutes
Dearonski · 2026-05-18 · via DEV Community

Next.js + Strapi is one of the best stacks for content-driven sites in 2026. Server Components, ISR, app router, and Strapi v5's clean REST API just click together.

But there's one part that still hurts: typing the data.

You ship your Strapi schema, then sit down in Next.js and… you're back to writing interfaces by hand. Then you forget to populate a relation and your category.name access blows up at runtime in a Server Component, which means a full page error instead of a clean TS warning.

Today we'll fix all of that — end to end — with a small open-source package called strapi-typed-client. By the end of this tutorial you'll have:

  • A typed Strapi client with full autocomplete
  • Types that regenerate automatically on next dev and next build
  • Populate calls where TypeScript knows exactly which nested fields are available
  • A real working Next.js 15 app reading from Strapi v5

No manual interfaces. No as any. No GraphQL.

Let's build it.


What we'll build

A tiny blog. Two content types in Strapi:

  • Articletitle, slug, body (rich text), cover (media), category (relation), author (relation)
  • Categoryname, slug

And two pages in Next.js:

  • / — list of latest articles with cover image and category
  • /articles/[slug] — full article with author bio

Standard stuff. The interesting part is everything in between.


Step 1 — Install the plugin on the Strapi side

In your Strapi v5 project:

npm install strapi-typed-client

Enter fullscreen mode Exit fullscreen mode

Then enable it in config/plugins.ts:

// config/plugins.ts
export default {
  'strapi-typed-client': {
    enabled: true,
  },
}

Enter fullscreen mode Exit fullscreen mode

Restart Strapi. The plugin exposes your schema at GET /api/strapi-typed-client/schema plus a live /schema-watch SSE endpoint we'll use later.

That's the whole server side.


Step 2 — Install the same package in your Next.js app

npm install strapi-typed-client

Enter fullscreen mode Exit fullscreen mode

Yes, the same package. It contains both the Strapi plugin and the Next.js client — the right entry point gets used depending on where you import from.


Step 3 — Hook it into next.config.ts

This is the part that does the magic:

// next.config.ts
import type { NextConfig } from 'next'
import { withStrapiTypes } from 'strapi-typed-client/next'

const nextConfig: NextConfig = {
  // your existing Next.js config
}

export default withStrapiTypes({
  strapiUrl: process.env.NEXT_PUBLIC_STRAPI_URL ?? 'http://localhost:1337',
  token: process.env.STRAPI_TOKEN, // optional, only if your plugin requires auth
})(nextConfig)

Enter fullscreen mode Exit fullscreen mode

What this does:

  • On next dev — connects to Strapi via SSE and regenerates types the moment your schema changes. Add a field in Strapi admin → TS knows about it before you tab back to your editor.
  • On next build — runs a one-time sync generation before the build kicks off.
  • On next start — does nothing (types are already in place).

No separate watcher process. No concurrently. No predev script. It just works as part of your normal Next.js lifecycle.


Step 4 — Generate types for the first time

You can let next dev do it automatically, or trigger it manually:

npx strapi-types generate --url http://localhost:1337

Enter fullscreen mode Exit fullscreen mode

Generated files land in node_modules/strapi-typed-client/dist. (If you'd rather commit the generated files into your repo, use --format ts --output ./src/strapi — handy for monorepos.)


Step 5 — Build the home page

Here's the entire app/page.tsx:

// app/page.tsx
import Image from 'next/image'
import Link from 'next/link'
import { StrapiClient } from 'strapi-typed-client'

const strapi = new StrapiClient({
  baseURL: process.env.NEXT_PUBLIC_STRAPI_URL ?? 'http://localhost:1337',
})

export default async function HomePage() {
  const { data: articles } = await strapi.articles.find({
    sort: ['publishedAt:desc'],
    pagination: { page: 1, pageSize: 10 },
    populate: {
      cover: true,
      category: true,
    },
  })

  return (
    <main>
      <h1>Latest articles</h1>
      <ul>
        {articles.map((article) => (
          <li key={article.documentId}>
            <Link href={`/articles/${article.slug}`}>
              {article.cover && (
                <Image
                  src={article.cover.url}
                  alt={article.cover.alternativeText ?? ''}
                  width={400}
                  height={200}
                />
              )}
              <h2>{article.title}</h2>
              <span>{article.category.name}</span>
            </Link>
          </li>
        ))}
      </ul>
    </main>
  )
}

Enter fullscreen mode Exit fullscreen mode

Pause and look at article.category.name. Try it in your editor. Hover over category — TypeScript knows it's the populated Category entity, not number | Category | undefined. Hover over cover — typed as Media, with url and alternativeText.

Now delete category: true from the populate object. TypeScript immediately marks article.category.name as an error: Property 'name' does not exist on type 'number'.

This is the part nobody else does. The whole point of Strapi's populate API is that the response shape depends on what you asked for. Most clients just type it as a union and shrug. This one infers the actual returned shape from your populate input.


Step 6 — Build the article detail page

// app/articles/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { StrapiClient } from 'strapi-typed-client'

const strapi = new StrapiClient({
  baseURL: process.env.NEXT_PUBLIC_STRAPI_URL ?? 'http://localhost:1337',
})

export default async function ArticlePage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params

  const { data: articles } = await strapi.articles.find({
    filters: { slug: { $eq: slug } },
    populate: {
      cover: true,
      category: true,
      author: {
        populate: { avatar: true },
      },
    },
  })

  const article = articles[0]
  if (!article) notFound()

  return (
    <article>
      <h1>{article.title}</h1>
      <div>
        <img src={article.author.avatar.url} alt={article.author.name} />
        <span>by {article.author.name}</span>
      </div>
      <p>{article.body}</p>
    </article>
  )
}

Enter fullscreen mode Exit fullscreen mode

Note the nested populate for author.avatar. The returned type is inferred all the way down — article.author.avatar.url is fully typed, but if you remove the inner populate: { avatar: true }, accessing .url becomes a TS error.


Step 7 — Filters and operators (also typed)

Strapi's filter operators ($eq, $contains, $gte, $or, etc.) are typed per field:

const { data } = await strapi.articles.find({
  filters: {
    title: { $contains: 'typescript' },
    publishedAt: { $gte: '2026-01-01' },
    $or: [
      { featured: { $eq: true } },
      { category: { slug: { $eq: 'editors-pick' } } },
    ],
  },
})

Enter fullscreen mode Exit fullscreen mode

Try writing views: { $contains: 'foo' } on a numeric field — TypeScript stops you. No more Invalid filter operator runtime errors from Strapi.


Step 8 — Caching and revalidation

Since the client uses the global fetch, all of Next.js's caching mechanics apply automatically. Pass a second argument to opt in:

// Always fresh
await strapi.articles.find({ /* params */ }, { cache: 'no-store' })

// ISR — revalidate every 60 seconds
await strapi.articles.find({ /* params */ }, { next: { revalidate: 60 } })

// Tag-based revalidation
await strapi.articles.find(
  { /* params */ },
  { next: { tags: ['articles'] } },
)

// Then somewhere in a Server Action:
import { revalidateTag } from 'next/cache'
revalidateTag('articles')

Enter fullscreen mode Exit fullscreen mode

These options are fully typed — same shape Next.js's fetch accepts.


Step 9 — The "rename a field" moment

This is the moment that sells the whole thing.

  1. Keep next dev running.
  2. Open Strapi admin in another tab. Rename title to headline on the Article content type. Save.
  3. Tab back to VS Code.

Within a second or two, every article.title in your codebase lights up red. The watcher caught the schema change, regenerated types, and TypeScript re-checked your project.

No restart. No manual command. No "wait, what did the field used to be called?" archaeology.

This is what withStrapiTypes buys you, and it's the single biggest DX win in the whole package.


Step 10 — Handling errors properly

Server Components fail loudly when fetches throw. The client exports typed errors so you can handle them gracefully:

import { StrapiClient, StrapiError, StrapiConnectionError } from 'strapi-typed-client'

try {
  const { data } = await strapi.articles.find()
  return <ArticleList articles={data} />
} catch (error) {
  if (error instanceof StrapiConnectionError) {
    // Strapi is down — show a friendly fallback
    return <BackendOffline />
  }
  if (error instanceof StrapiError && error.status === 401) {
    // Token expired
    return <NeedAuth />
  }
  throw error
}

Enter fullscreen mode Exit fullscreen mode


Quick gotcha: as const for populate objects

If you extract a populate object to a variable, annotate it with as const — otherwise TS widens the literal trues to boolean, and the inference breaks:

// ❌ Wrong — inference breaks
const populate = { category: true, cover: true }

// ✅ Correct
const populate = { category: true, cover: true } as const

await strapi.articles.find({ populate })

Enter fullscreen mode Exit fullscreen mode

Same pattern as satisfies for config objects. Once you know it, it's invisible.


What you got

Recap of what this 10-step tutorial set up:

  • One package, two install points (Strapi + Next.js)
  • Zero-config type generation tied to next dev and next build
  • Live regeneration on schema changes via SSE
  • Populate calls where the returned type follows the populate input
  • Typed filters, sorting, pagination
  • First-class Next.js caching options
  • Typed error classes for failure handling

No manual interfaces. No GraphQL. No second watcher process. Your Strapi schema is the single source of truth, and your Next.js code knows about it without you doing anything.


Try it

npm install strapi-typed-client

Enter fullscreen mode Exit fullscreen mode

That's the only command you need on either side — the same package powers both the Strapi plugin and the Next.js client.

Full docs live at boxlab-ltd.github.io/strapi-typed-client, source is on GitHub. If you run into anything weird, open an issue — I read all of them.

Now go rename a field in Strapi and watch TypeScript catch it. That part doesn't get old.