Combine Same.new's AI-powered Next.js code generation with Strapi 5's headless CMS to scaffold content-driven frontends faster. This guide covers setup, API integration, and a working project example.

Same.new is an AI-powered Next.js development platform built exclusively for Next.js applications. Backed by Y Combinator (Winter 2024) and powered by Claude 4.5 Opus, it translates natural language descriptions into complete React-based codebases, including components, pages, API routes, and project structure.
You describe what you need through a chat interface. Same.new generates the Next.js code using Claude 4.5 Opus. You can then make follow-up chat-based edits for further refinement or directly edit the generated code for customization. You can then export the entire project for independent deployment, where you would manually add Strapi API integration code if needed.
A few things worth knowing upfront:
Same.new generates Next.js frontend scaffolding through natural language prompts, while Strapi 5 provides a headless content management system (CMS) with auto-generated REST and GraphQL APIs.
Developers use Same.new to quickly bootstrap a Next.js application structure, then manually integrate Strapi's API endpoints into the generated project using standard fetch or axios patterns. This separation of concerns enables flexible content management through Strapi without being locked into a specific frontend technology. Here's why the combination works:
@strapi/client library (JavaScript/TypeScript SDK) designed for consuming REST and GraphQL APIs in Next.js applications. The patterns align for frontend integration without adaptation layers.The integration follows a practical pattern where you set up Strapi as your content backend, generate the Next.js frontend with Same.new using natural language prompts, then manually add API integration code to wire the two together using standard Next.js patterns for consuming Strapi's REST or GraphQL APIs.
Before starting, make sure you have:
Start by setting up a fresh Strapi 5 instance:
npx create-strapi@latest my-strapi-backendThe interactive installer prompts for configuration options. For local development, the defaults (SQLite database) work fine. Once installation completes, start the development server:
cd my-strapi-backend && npm run developThe Admin Panel launches at http://localhost:1337/admin. Create your administrator account on first visit.
Content types define the structure of your data and the auto-generated REST and GraphQL API responses your frontend will consume. Navigate to the Content-Type Builder in the Admin Panel and create the structures your application needs.
For this guide, create an Article Collection Type with these fields:
| Field Name | Type | Options |
|---|---|---|
| title | Text | Required |
| slug | UID | Attached to "title" |
| content | Rich Text | |
| excerpt | Text | |
| coverImage | Media | Single image |
| publishedAt | DateTime |
After saving, Strapi automatically generates REST endpoints at /api/articles for standard CRUD operations without requiring manual route handlers or controllers. GraphQL support requires explicit configuration in config/plugins.js to expose a /graphql endpoint.
While Strapi eliminates boilerplate for basic read and write operations, developers may still need to create custom controllers and services for business logic beyond standard CRUD, as well as configure API token permissions and role-based access control for security.
Add a few sample entries through the Content Manager so you have data to work with in later steps.
Your frontend needs to authenticate against Strapi's Content API since all content types are private by default. You can do this using API token authentication, JWT user authentication via the Users & Permissions plugin, or by explicitly configuring role-based permissions for the Authenticated role. API token authentication is the recommended approach for frontend applications.
Store this token securely. It goes into environment variables in your Next.js project, never into source code.
By default, Strapi's CORS middleware uses wildcard origins, which can cause issues with credential-based requests. Configure explicit origins in config/middlewares.ts (or .js):
export default [
'strapi::logger',
'strapi::errors',
{
name: 'strapi::cors',
config: {
origin: ['http://localhost:3000', 'https://your-production-domain.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
headers: ['Content-Type', 'Authorization', 'Origin', 'Accept'],
credentials: true,
},
},
'strapi::poweredBy',
'strapi::query',
'strapi::body',
'strapi::session',
'strapi::favicon',
'strapi::public',
];Replace the origin URLs with the actual domains where your frontend will run. For local development, http://localhost:3000 is the Next.js default.
Open same.new and describe the application you need. Be specific about the structure and components, since Same.new generates the entire Next.js project from your prompt.
An effective prompt for a blog frontend might look like this:
Build a blog application using Next.js App Router and TypeScript. Include a homepage that displays article cards in a grid layout with title, excerpt, cover image, and publication date. Include an individual article page with full content rendering. Use Tailwind CSS for styling. Add a responsive navigation bar and footer. The data will come from a Strapi v5 REST or GraphQL API, so integrate the official @strapi/client library with API token authentication to fetch article content types and related data.
Same.new generates complete Next.js projects through natural language prompts, including React component files, page routes with API routes, layouts, and styling configured with TypeScript or JavaScript.
After AI generates the initial project scaffold, developers can iteratively refine it through natural language chat instructions (e.g., "Add a contact form," "Change the color scheme") or by directly editing the generated code in the editor for fine-tuning and customization. Once satisfied, developers can export the complete, production-ready Next.js project for independent deployment.
Export options: You can push to GitHub or download the project files. Either way, you need local access to the codebase for the next steps.
Clone or extract your exported project, then install the official Strapi client:
cd my-same-new-blog
npm install @strapi/clientCreate an environment file for your API configuration:
# .env.local
STRAPI_API_URL=http://localhost:1337
STRAPI_API_TOKEN=your-api-token-from-step-3
NEXT_PUBLIC_STRAPI_API_URL=http://localhost:1337The STRAPI_API_TOKEN variable (without the NEXT_PUBLIC_ prefix) stays server-side only, keeping your token out of client bundles. The NEXT_PUBLIC_STRAPI_API_URL variant is safe for client-side use since it only contains the base URL.
Set up a centralized module for Strapi API interactions. Create lib/strapi.ts in your project:
import { strapi } from '@strapi/client';
const client = strapi({
baseURL: `${process.env.STRAPI_API_URL}/api`,
auth: process.env.STRAPI_API_TOKEN,
});
export async function getArticles(page: number = 1, pageSize: number = 10) {
const articles = await client.collection('articles').find({
populate: ['coverImage'],
sort: ['publishedAt:desc'],
pagination: {
page,
pageSize,
},
});
return articles;
}
export async function getArticleBySlug(slug: string) {
const articles = await client.collection('articles').find({
filters: {
slug: {
$eq: slug,
},
},
populate: ['coverImage'],
});
return articles.data?.[0] || null;
}This module encapsulates all Strapi communication. The @strapi/client library provides a typed interface for API token authentication and content querying, requiring explicit configuration of the API base URL and authentication token.
Replace the placeholder data fetching in your Same.new-generated pages with the Strapi service functions. Here's what the articles listing page looks like:
// app/page.tsx
import { getArticles } from '@/lib/strapi';
import ArticleCard from '@/components/ArticleCard';
export default async function HomePage() {
const { data: articles, meta } = await getArticles();
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">Latest Articles</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{articles.map((article) => (
<ArticleCard
key={article.documentId}
title={article.title}
excerpt={article.excerpt}
slug={article.slug}
coverImage={article.coverImage}
publishedAt={article.publishedAt}
/>
))}
</div>
</main>
);
}This component uses server-side data fetching compatible with Next.js 13+ App Router. Note that coverImage must be explicitly populated (as configured in the service module), since Strapi v5 doesn't populate relations or media fields by default.
The component uses documentId as the key, which is Strapi v5's primary identifier in its flattened response structure.
And the individual article page:
// app/articles/[slug]/page.tsx
import { getArticleBySlug, getArticles } from '@/lib/strapi';
import { notFound } from 'next/navigation';
interface ArticlePageProps {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
const { data: articles } = await getArticles(1, 100);
return articles.map((article) => ({
slug: article.slug,
}));
}
export default async function ArticlePage({ params }: ArticlePageProps) {
const { slug } = await params;
const article = await getArticleBySlug(slug);
if (!article) {
notFound();
}
return (
<article className="container mx-auto px-4 py-8 max-w-3xl">
{article.coverImage && (
<img
src={`${process.env.NEXT_PUBLIC_STRAPI_API_URL}${article.coverImage.url}`}
alt={article.coverImage.alternativeText || article.title}
className="w-full rounded-lg mb-8"
/>
)}
<h1 className="text-4xl font-bold mb-4">{article.title}</h1>
<time className="text-gray-500 mb-8 block">
{new Date(article.publishedAt).toLocaleDateString()}
</time>
<div
className="prose prose-lg"
dangerouslySetInnerHTML={{ __html: article.content }}
/>
</article>
);
}Note a few things about Strapi 5's response structure: attributes are flattened directly onto the data object (no nested attributes key), and each document has both a numeric id and a string documentId. Use documentId as your primary identifier for API operations.
Start both services and confirm data flows correctly:
Terminal 1 (Strapi):
cd my-strapi-backend && npm run developTerminal 2 (Next.js):
cd my-same-new-blog && npm run devOpen http://localhost:3000 in your browser. You should see your Strapi content rendered through the Same.new-generated UI. If articles aren't appearing, check these common issues:
.env.local and that the token has the appropriate permissions for the content type you're querying. When querying related content types, ensure the authenticated role has read permissions on both the primary content type and all related content types.http://localhost:3000 is listed in your CORS origin configuration in config/middlewares.ts. Ensure you're using explicit origin values in arrays rather than wildcard configurations when making credential-based requests.populate parameter with specific relation names or use populate=* for all relations.Let's build something more complete: a developer blog where Same.new generates the frontend and Strapi manages articles, authors, and categories. This example demonstrates relational content, filtered queries, and dynamic routing.
Create Collection Types in the Content-Type Builder according to your content requirements. For example, in a recipe application, you might create Collection Types for "Recipe", "Author", and "Category" content types. Use the GUI-based interface or code-based schema definition methods documented in the Content-Type Builder to design your content structure.
Author:
| Field | Type | Options |
|---|---|---|
| name | Text | Required |
| bio | Text | |
| avatar | Media | Single |
Category:
| Field | Type | Options |
|---|---|---|
| name | Text | Required |
| slug | UID | Required |
Post:
| Field | Type | Options |
|---|---|---|
| title | Text | Required |
| slug | UID | Attached to "title" |
| content | Rich Text | |
| excerpt | Text | |
| coverImage | Media | Single image |
| author | Relation | Many-to-One with Author |
| categories | Relation | Many-to-Many with Category |
After saving, configure permissions under Settings → Users & Permissions plugin → Roles → Public. Enable find and findOne for Post, Author, and Category. This is critical: when your query populates the author relation, Strapi requires that the authenticated user's role has read permissions on both the primary content type (Post) and all related content types referenced in the query (Author and Category). If the Public role doesn't have read access to these related content types, relation fields will return as empty.
Use this prompt in Same.new:
Create a developer blog with Next.js App Router and TypeScript using Tailwind CSS. The blog should have: a homepage showing featured and recent posts in a card grid, a category page that filters posts by category, individual post pages with author bio sidebar, a top navigation bar with links to Home and categories. Use a dark theme. Make data fetching functions modular so I can connect an external CMS. Use server components for data fetching.
Export the generated project and install dependencies:
cd developer-blog
npm install @strapi/client qs
npm install --save-dev @types/qsCreate a comprehensive service module at lib/strapi.ts:
import { strapi } from '@strapi/client';
const client = strapi({
baseURL: `${process.env.STRAPI_API_URL}/api`,
auth: process.env.STRAPI_API_TOKEN,
});
export async function getPosts(page = 1, pageSize = 9) {
return client.collection('posts').find({
populate: ['author', 'coverImage', 'categories'],
sort: ['publishedAt:desc'],
pagination: { page, pageSize },
});
}
export async function getPostBySlug(slug: string) {
const result = await client.collection('posts').find({
filters: { slug: { $eq: slug } },
populate: ['coverImage', 'author', 'author.avatar', 'categories'],
});
return result.data?.[0] || null;
}
export async function getPostsByCategory(categorySlug: string, page = 1) {
const result = await client.collection('posts').find({
filters: {
categories: {
slug: { $eq: categorySlug },
},
},
populate: ['coverImage', 'author', 'categories'],
sort: ['publishedAt:desc'],
pagination: { page, pageSize: 9 },
});
return result;
}
export async function getCategories() {
return client.collection('categories').find({
sort: ['name:asc'],
});
}
export async function getFeaturedPosts() {
return client.collection('posts').find({
populate: ['coverImage', 'author', 'categories'],
sort: ['publishedAt:desc'],
pagination: { page: 1, pageSize: 3 },
});
}For queries with deeply nested populations, the qs library helps you construct properly formatted URLs without manual string manipulation, as recommended in the Strapi REST API parameters guide.
import qs from 'qs';
export async function getPostWithDeepRelations(slug: string) {
const query = qs.stringify(
{
filters: { slug: { $eq: slug } },
populate: {
coverImage: { fields: ['url', 'alternativeText', 'width', 'height'] },
author: {
populate: ['avatar'],
},
categories: {
fields: ['name', 'slug'],
},
},
},
{ encodeValuesOnly: true }
);
const response = await fetch(
`${process.env.STRAPI_API_URL}/api/posts?${query}`,
{
headers: {
Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}`,
},
next: { revalidate: 60 },
}
);
if (!response.ok) {
throw new Error(`Failed to fetch post: ${response.status}`);
}
const { data } = await response.json();
return data?.[0] || null;
}The next: { revalidate: 60 } option enables Incremental Static Regeneration (ISR), revalidating cached data every 60 seconds before re-fetching. This provides static-like performance with content that stays reasonably fresh.
Connect the category filter page to Strapi:
// app/category/[slug]/page.tsx
import { getPostsByCategory, getCategories } from '@/lib/strapi';
import PostGrid from '@/components/PostGrid';
import { notFound } from 'next/navigation';
interface CategoryPageProps {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
const { data: categories } = await getCategories();
return categories.map((cat) => ({ slug: cat.slug }));
}
export default async function CategoryPage({ params }: CategoryPageProps) {
const { slug } = await params;
const { data: posts, meta } = await getPostsByCategory(slug);
if (!posts.length) {
notFound();
}
const categoryName = posts[0]?.categories?.find(
(c: { slug: string }) => c.slug === slug
)?.name;
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-2">{categoryName || slug}</h1>
<p className="text-gray-400 mb-8">
{meta.pagination.total} {meta.pagination.total === 1 ? 'post' : 'posts'}
</p>
<PostGrid posts={posts} />
</main>
);
}// app/posts/[slug]/page.tsx
import { getPostBySlug, getPosts } from '@/lib/strapi';
import { notFound } from 'next/navigation';
interface PostPageProps {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
const { data: posts } = await getPosts(1, 50);
return posts.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({ params }: PostPageProps) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) {
notFound();
}
const strapiUrl = process.env.NEXT_PUBLIC_STRAPI_API_URL;
return (
<div className="container mx-auto px-4 py-8 flex flex-col lg:flex-row gap-8">
<article className="lg:w-2/3">
{post.coverImage && (
<img
src={`${strapiUrl}${post.coverImage.url}`}
alt={post.coverImage.alternativeText || post.title}
className="w-full rounded-lg mb-6"
/>
)}
<div className="flex gap-2 mb-4">
{post.categories?.map((cat: { documentId: string; name: string; slug: string }) => (
<a
key={cat.documentId}
href={`/category/${cat.slug}`}
className="text-sm bg-blue-900 text-blue-200 px-3 py-1 rounded-full"
>
{cat.name}
</a>
))}
</div>
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<time className="text-gray-500 block mb-8">
{new Date(post.publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
<div
className="prose prose-invert prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
{post.author && (
<aside className="lg:w-1/3">
<div className="sticky top-8 bg-gray-800 rounded-lg p-6">
{post.author.avatar && (
<img
src={`${strapiUrl}${post.author.avatar.url}`}
alt={post.author.name}
className="w-20 h-20 rounded-full mb-4"
/>
)}
<h3 className="text-lg font-semibold">{post.author.name}</h3>
{post.author.bio && (
<p className="text-gray-400 mt-2 text-sm">{post.author.bio}</p>
)}
</div>
</aside>
)}
</div>
);
}For production, deploy each service independently:
Strapi backend: Deploy to Strapi Cloud, a VPS, or any Node.js-compatible platform. Set your production environment variables:
NODE_ENV=production
DATABASE_URL=postgresql://user:password@host:5432/dbname
ADMIN_JWT_SECRET=your-admin-secret
JWT_SECRET=your-jwt-secret
APP_KEYS=key1,key2,key3,key4
API_TOKEN_SALT=your-token-saltNext.js frontend: Deploy to Vercel, Netlify, or any platform supporting Next.js. Update environment variables to point at your production Strapi URL:
STRAPI_API_URL=https://your-strapi-backend.com
STRAPI_API_TOKEN=your-production-api-token
NEXT_PUBLIC_STRAPI_API_URL=https://your-strapi-backend.comRemember to update your Strapi CORS configuration with the production frontend domain before deploying. According to the Strapi v5 CORS configuration documentation, you must explicitly configure allowed origins in your config/middlewares.ts file with a structure like:
origin: ['http://localhost:3000', 'https://yourdomain.com']This replaces the default wildcard origin, which can cause conflicts when making credential-based requests from your frontend.
Next Steps
You now have a working integration where Same.new provides the frontend scaffold and Strapi manages structured content through auto-generated APIs. From here, consider adding webhook-based revalidation so your Next.js pages update automatically when content changes in Strapi.
You can also explore Strapi's Draft & Publish workflow for content preview before going live, or add search functionality using Strapi's filtering and full-text search capabilities.
If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.
For more details, visit the Strapi documentation for comprehensive guidance on Strapi v5 features including REST/GraphQL APIs, content modeling, authentication, and deployment, and the Same.new documentation for details on AI-powered Next.js development, integrations with Neon, Supabase, and Clerk, and natural language prompting capabilities.
No. Same.new operates entirely through a natural language web interface and doesn't make external API calls to services like Strapi during generation. The workflow is sequential: Same.new scaffolds the Next.js frontend, then you export the project and manually add Strapi API integration code using the @strapi/client library or standard fetch calls. This gives you full control over authentication, error handling, and data mapping rather than relying on auto-generated API connections.
Yes. Strapi v5 auto-generates REST endpoints for every content type, and you can enable GraphQL by installing the GraphQL plugin. Same.new generates standard Next.js projects, which can consume either API style through manual integration of Strapi API calls. REST is simpler for most use cases, while GraphQL reduces over-fetching when you need precise control over response shapes. One limitation: media file uploads require the REST API even when using GraphQL elsewhere.
Strapi stores media files locally by default and returns relative URLs like /uploads/image.jpg. You need to prepend the Strapi base URL in your components, and media fields must be explicitly populated since they aren't included by default. For production, consider using a cloud storage provider like Cloudinary or AWS S3 through Strapi's upload provider plugins. Configure next.config.js with the appropriate images.remotePatterns entry so Next.js Image Optimization works with your Strapi domain.
Strapi v5 introduced a flattened response structure where each document includes a numeric id (the database primary key) and a string documentId (a unique identifier). When making API operations — fetching, updating, or deleting specific documents — use the documentId in endpoint paths, as this is the identifier Strapi v5 expects (e.g., /api/articles/:documentId). The numeric id remains available in responses for reference, but documentId is the required identifier for all programmatic API operations in v5.
Same.new does not provide programmatic APIs for accessing or regenerating existing projects. Any Strapi integration code you added manually (API service modules, data fetching logic, environment variables) exists only in your local codebase, not within Same.new's platform. The practical workflow is to maintain your integration code locally and request new components or pages from Same.new separately through natural language prompts, then manually integrate those additions with your existing Strapi data layer in your local development environment.