These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
What Is Vite?
Vite is a frontend build tool that takes a fundamentally different approach than traditional bundlers. Instead of bundling your entire application before serving it, Vite leverages native ES modules in the browser to serve source code on-demand.
The tool consists of two major components: a development server that provides instant startup and hot module replacement, and a build command that uses Rollup for optimized production bundles. During development, Vite pre-bundles dependencies using esbuild—which pre-bundles dependencies 10-100x faster than JavaScript-based bundlers—while serving your application code directly to the browser as native ES modules on-demand.
This architecture means server startup time stays consistent regardless of application size. When you edit a file, Vite only invalidates the specific module chain affected, keeping HMR fast even in large codebases.
Sign up for the Logbook, Strapi's Monthly newsletter
Why Integrate Vite with Strapi
Combining Vite's build performance with Strapi's content management features creates a development experience optimized for content-driven applications. Here's what makes this pairing effective:
- Development speed: Vite's sub-second server startup pairs with Strapi's auto-generated APIs to eliminate waiting time during content modeling iterations.
- Hot Module Replacement: Changes to frontend components reflect instantly while Strapi's Admin Panel runs independently, enabling parallel content and code development.
- Modern JavaScript support: Both tools embrace ES modules natively, reducing configuration overhead and build complexity.
- Framework flexibility: Vite's official templates for React, Vue, and Svelte integrate cleanly with Strapi's framework-agnostic REST and GraphQL endpoints.
- Production optimization: Vite's Rollup-based production builds create optimized bundles that pair well with Strapi's CDN-friendly media handling.
- TypeScript integration: Both tools provide first-class TypeScript support for type-safe content fetching and component development.
How to Integrate Vite with Strapi
Prerequisites
Before starting, verify your environment meets these requirements:
- Node.js 20.19+: Both Strapi v5 and Vite require this as the minimum version.
- Database: MySQL 8.4+, MariaDB 11.4+, PostgreSQL 14+, or SQLite 3.
- Package manager: npm, yarn, or pnpm.
- Text editor: VS Code with TypeScript support recommended.
You should be comfortable with JavaScript/TypeScript, REST APIs, and basic command line operations.
Step 1: Create the Strapi Backend
Start by scaffolding a new Strapi v5 project. Open your terminal and run:
npx create-strapi@latest my-strapi-backendThe CLI walks you through an interactive setup where you select your database and configure credentials. Once installation completes, start the development server:
cd my-strapi-backend
npm run developStrapi runs on port 1337 by default. The admin panel opens at http://localhost:1337/admin, where you create your first administrator account.
For this integration, create a simple content type to work with. Navigate to the Content-Type Builder in the admin panel and create a collection type called "Article" with these fields:
title(Text, required)content(Rich Text)slug(UID, attached to title)publishedAt(DateTime)
After creating the content type, add a few sample articles through the Content Manager.
Step 2: Create the Vite Frontend
In a separate terminal, scaffold a new Vite project:
npm create vite@latest my-vite-frontendSelect your preferred framework (React, Vue, or Svelte) and variant (JavaScript or TypeScript). This guide uses React with TypeScript for examples, but the patterns apply to any framework.
Install dependencies and start the development server:
cd my-vite-frontend
npm install
npm run devVite runs on port 5173 by default. You now have two development servers running: Strapi on 1337 and Vite on 5173.
Step 3: Configure CORS in Strapi
This is where most integration issues occur. Strapi requires explicit CORS configuration to allow requests from your Vite frontend, so your development server on port 5173 needs to be added to the allowed origins in Strapi's middleware configuration.
Open config/middlewares.js in your Strapi project and replace its contents:
module.exports = [
'strapi::logger',
'strapi::errors',
'strapi::security',
{
name: 'strapi::cors',
config: {
origin: ['http://localhost:5173'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
headers: ['Content-Type', 'Authorization', 'Origin', 'Accept'],
keepHeaderOnError: true,
},
},
'strapi::poweredBy',
'strapi::query',
'strapi::body',
'strapi::session',
'strapi::favicon',
'strapi::public',
];The origin array must include the exact URL of your Vite development server. For production deployments, add your production frontend URL to this array.
Restart the Strapi server for the changes to take effect.
Step 4: Set Up Environment Variables
Create a .env file in your Vite project root. Remember that only variables prefixed with VITE_ are exposed to client-side code:
VITE_API_URL=http://localhost:1337
VITE_API_TOKEN=your_api_token_hereTo create an API token, go to Settings → API Tokens in the Strapi admin panel. Create a new token with "Read-only" permissions for public content fetching. This token grants access to your content without exposing admin credentials.
Access these variables in your code using import.meta.env:
const API_URL = import.meta.env.VITE_API_URL;Step 5: Configure Vite Proxy (Optional)
A proxy configuration simplifies API calls during development by routing requests through Vite's server. This approach eliminates CORS issues entirely for development:
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:1337',
changeOrigin: true,
secure: false,
ws: true,
}
},
cors: true,
}
})With proper Vite proxy configuration in vite.config.js, fetch calls to /api/articles automatically route to http://localhost:1337/api/articles during development. However, this development convenience requires careful CORS configuration in Strapi's config/middlewares.js to allow requests from the Vite development server (http://localhost:5173). For production, the proxy becomes unnecessary as both frontend and backend share the same domain or you must use explicit API URL configuration instead.
Step 6: Create the API Service
Build a centralized API service to handle all Strapi communication. Create src/services/api.ts:
// src/services/api.ts
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:1337';
const API_TOKEN = import.meta.env.VITE_API_TOKEN;
interface StrapiResponse<T> {
data: T;
meta: {
pagination?: {
page: number;
pageSize: number;
pageCount: number;
total: number;
};
};
}
interface Article {
id: number;
documentId: string;
title: string;
content: string;
slug: string;
publishedAt: string;
}
export async function fetchArticles(): Promise<Article[]> {
const response = await fetch(`${API_URL}/api/articles?populate=*`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch articles: ${response.statusText}`);
}
const result: StrapiResponse<Article[]> = await response.json();
return result.data;
}
export async function fetchArticleBySlug(slug: string): Promise<Article | null> {
const response = await fetch(
`${API_URL}/api/articles?filters[slug][$eq]=${slug}&populate=*`,
{
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
}
);
if (!response.ok) {
throw new Error(`Failed to fetch article: ${response.statusText}`);
}
const result: StrapiResponse<Article[]> = await response.json();
return result.data[0] || null;
}Note on Strapi v5 response structure: Data attributes exist directly on the object—access article.title instead of article.attributes.title. This flattened structure simplifies data handling compared to v4.
Step 7: Consume Content in Components
Create a component that fetches and displays articles from Strapi v5. Here's a React example using Vite:
// src/components/ArticleList.tsx
import { useEffect, useState } from 'react';
import { fetchArticles } from '../services/api';
interface Article {
id: number;
documentId: string;
title: string;
content: string;
slug: string;
publishedAt: string;
}
export function ArticleList() {
const [articles, setArticles] = useState<Article[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadArticles = async () => {
try {
const data = await fetchArticles();
setArticles(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load articles');
} finally {
setLoading(false);
}
};
loadArticles();
}, []);
if (loading) return <div>Loading articles...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div className="articles-grid">
{articles.map((article) => (
<article key={article.documentId}>
<h2>{article.title}</h2>
<time dateTime={article.publishedAt}>
{new Date(article.publishedAt).toLocaleDateString()}
</time>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
))}
</div>
);
}Step 8: Handle Media Assets
When working with images from Strapi's Media Library, file URLs are relative paths. Construct full URLs by prepending the Strapi base URL:
interface MediaFile {
id: number;
name: string;
url: string;
mime: string;
size: number;
formats?: {
thumbnail?: { url: string };
small?: { url: string };
medium?: { url: string };
large?: { url: string };
};
}
function getImageUrl(media: MediaFile, size?: 'thumbnail' | 'small' | 'medium' | 'large'): string {
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:1337';
if (!media || !media.url) {
console.warn('Invalid media object provided to getImageUrl');
return '';
}
let imagePath = media.url;
// Use the requested size format if available
if (size && media.formats?.[size]?.url) {
imagePath = media.formats[size].url;
}
// Construct full URL by prepending the Strapi base URL
return `${baseUrl}${imagePath}`;
}This pattern lets you request appropriately sized images based on display context—thumbnails for lists, larger formats for detail views.
Project Example: Content Blog with Dynamic Routing
Let's build a functional blog that demonstrates the full integration. This project uses React with React Router, but the concepts translate to Vue Router or SvelteKit.
Project Structure
vite-strapi-blog/
├── src/
│ ├── components/
│ │ ├── ArticleCard.tsx
│ │ ├── ArticleDetail.tsx
│ │ └── Layout.tsx
│ ├── services/
│ │ └── api.ts
│ ├── hooks/
│ │ └── useArticles.ts
│ ├── pages/
│ │ ├── Home.tsx
│ │ └── Article.tsx
│ ├── App.tsx
│ └── main.tsx
├── vite.config.ts
├── .env
└── package.jsonCustom Data Fetching Hook
Create a reusable hook for article fetching with loading and error states:
// src/hooks/useArticles.ts
import { useEffect, useState } from 'react';
import { fetchArticles, fetchArticleBySlug } from '../services/api';
interface Article {
id: number;
documentId: string;
title: string;
content: string;
slug: string;
publishedAt: string;
coverImage?: {
url: string;
formats?: Record<string, { url: string }>;
};
}
export function useArticles() {
const [articles, setArticles] = useState<Article[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
const load = async () => {
try {
const data = await fetchArticles();
if (!cancelled) {
setArticles(data);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err : new Error('Unknown error'));
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
load();
return () => {
cancelled = true;
};
}, []);
return { articles, loading, error };
}
export function useArticle(slug: string) {
const [article, setArticle] = useState<Article | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
const load = async () => {
try {
const data = await fetchArticleBySlug(slug);
if (!cancelled) {
setArticle(data);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err : new Error('Unknown error'));
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
load();
return () => {
cancelled = true;
};
}, [slug]);
return { article, loading, error };
}Enhanced API Service with Filtering and Pagination
Expand the API service to support Strapi v5's query parameters including filtering operators ($eq, $ne, $lt, $lte, $gt, $gte, $in, $notIn, $contains, $notContains, $containsi, $startsWith, $endsWith, $null, $notNull), the populate parameter for explicit relation fetching, pagination options (page/pageSize or start/limit), sorting capabilities, field selection, locale parameters, and draft/publish status control.
// src/services/api.ts
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:1337';
const API_TOKEN = import.meta.env.VITE_API_TOKEN;
interface FetchOptions {
page?: number;
pageSize?: number;
sort?: string;
filters?: Record<string, string>;
}
export async function fetchArticles(options: FetchOptions = {}) {
const { page = 1, pageSize = 10, sort = 'publishedAt:desc', filters = {} } = options;
const params = new URLSearchParams({
'populate': '*',
'pagination[page]': page.toString(),
'pagination[pageSize]': pageSize.toString(),
'sort': sort,
});
// Add filters
Object.entries(filters).forEach(([key, value]) => {
params.append(`filters[${key}][$eq]`, value);
});
const response = await fetch(`${API_URL}/api/articles?${params}`, {
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}// Fetch only published articles from 2024
const articles = await fetchArticles({
filters: {
'publishedAt[$gte]': '2024-01-01',
'status': 'published',
},
sort: 'publishedAt:desc',
pageSize: 5,
});Working with Dynamic Zones
If your content type uses Dynamic Zones for flexible content blocks, you must explicitly populate them in your API requests and implement conditional rendering logic in your frontend:
interface DynamicComponent {
__component: string;
id: number;
[key: string]: unknown;
}
interface RichTextBlock extends DynamicComponent {
__component: 'blocks.rich-text';
content: string;
}
interface MediaBlock extends DynamicComponent {
__component: 'blocks.media';
file: { url: string };
caption?: string;
}
type ContentBlock = RichTextBlock | MediaBlock;
function renderDynamicZone(components: ContentBlock[]) {
return components.map((component) => {
switch (component.__component) {
case 'blocks.rich-text':
return <div key={component.id} dangerouslySetInnerHTML={{ __html: (component as RichTextBlock).content }} />;
case 'blocks.media':
const media = component as MediaBlock;
return <img key={component.id} src={getImageUrl(media.file)} alt={media.caption || ''} />;
default:
return null;
}
});
}The __component discriminator field tells you which component type to render, enabling type-safe handling of varied content structures.
Production Build Configuration
For production deployment, update your environment variables and build configuration:
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'react-router-dom'],
},
},
},
},
})# .env.production
VITE_API_URL=https://your-strapi-production-url.com
VITE_API_TOKEN=your_production_api_tokenBuild the production bundle:
npm run buildDeploy the dist folder to any static hosting service—Vercel, Netlify, or a CDN. The Strapi backend deploys separately to Node.js hosting. Strapi Cloud or third-party providers like Railway or Render work well for the backend.
With Vite handling your frontend builds and Strapi managing your content, you have a modern stack optimized for content-driven applications. The patterns covered here—API service abstraction, proper CORS configuration, and typed responses—scale from simple blogs to complex multi-content-type applications. Explore Strapi's plugin ecosystem to extend functionality as your project grows.
Strapi Open Office Hours
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 and Vite documentation.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.