Prerequisites :
Nuxt 3.0 or higher
TypeScript enabled in your Nuxt project
Strapi v4 or v5 backend
Strapi2Front generates TypeScript types, services, and Zod schemas for your Nuxt applications. Server Routes generation is coming soon.
Current Support
TypeScript Types Fully typed interfaces for all content types
Service Functions Ready-to-use data fetching functions
Zod Schemas Validation schemas for forms and mutations
Server Routes Coming soon in a future release
Setup
Install Strapi2Front
Install the package in your Nuxt project:
Initialize Configuration
Run the init command to create a configuration file:
Configure for Nuxt
Update your strapi2front.config.ts: import { defineConfig } from 'strapi2front' ;
export default defineConfig ({
strapiUrl: process . env . NUXT_PUBLIC_STRAPI_URL || 'http://localhost:1337' ,
strapiVersion: 'v5' , // or 'v4'
output: {
path: './composables/strapi' ,
structure: 'by-feature' ,
} ,
features: {
types: true ,
services: true ,
schemas: true ,
// Server Routes coming soon
} ,
}) ;
Add Environment Variables
Create a .env file: NUXT_PUBLIC_STRAPI_URL = https://your-strapi-instance.com
STRAPI_API_TOKEN = your-api-token-here
Generate Code
Run the sync command: This generates: composables/strapi/
├── collections/
│ └── article/
│ ├── types.ts
│ ├── schemas.ts
│ └── service.ts
├── singles/
│ └── homepage/
│ ├── types.ts
│ ├── schemas.ts
│ └── service.ts
└── shared/
├── client.ts
└── utils.ts
Usage
Server-Side Rendering
Fetch data in your pages using useAsyncData or useFetch:
pages/blog/index.vue
pages/blog/[slug].vue
< script setup lang = "ts" >
import { articleService } from '~/composables/strapi/collections/article/service' ;
const page = ref ( 1 );
const pageSize = 10 ;
// Fetch articles with SSR
const { data : articlesData , pending , error } = await useAsyncData (
`articles-page- ${ page . value } ` ,
async () => {
return await articleService . findMany ({
pagination: {
page: page . value ,
pageSize ,
},
sort: [ 'publishedAt:desc' ],
populate: [ 'author' , 'coverImage' ],
});
},
{
watch: [ page ],
}
);
const articles = computed (() => articlesData . value ?. data || []);
const pagination = computed (() => articlesData . value ?. pagination );
</ script >
< template >
< div >
< div v-if = " pending " > Loading... </ div >
< div v-else-if = " error " > Error loading articles </ div >
< div v-else class = "blog-grid" >
< article v-for = " article in articles " : key = " article . documentId " >
< h2 > {{ article . title }} </ h2 >
< p > {{ article . excerpt }} </ p >
< NuxtLink : to = " `/blog/ ${ article . slug } ` " >
Read more
</ NuxtLink >
</ article >
< Pagination
: current-page = " pagination . page "
: total-pages = " pagination . pageCount "
@ update : page = " page = $event "
/>
</ div >
</ div >
</ template >
Client-Side Fetching
Fetch data on the client with reactivity:
components/ArticleList.vue
< script setup lang = "ts" >
import type { Article } from '~/composables/strapi/collections/article/types' ;
const articles = ref < Article []>([]);
const loading = ref ( true );
const error = ref < Error | null >( null );
onMounted ( async () => {
try {
const { data } = await $fetch ( '/api/articles' );
articles . value = data ;
} catch ( e ) {
error . value = e as Error ;
} finally {
loading . value = false ;
}
});
</ script >
< template >
< div >
< div v-if = " loading " > Loading... </ div >
< div v-else-if = " error " > Error: {{ error . message }} </ div >
< div v-else class = "article-list" >
< div v-for = " article in articles " : key = " article . documentId " >
< h3 > {{ article . title }} </ h3 >
< p > {{ article . excerpt }} </ p >
</ div >
</ div >
</ div >
</ template >
Server API Routes
Create API routes using the generated services:
server/api/articles/index.get.ts
server/api/articles/[id].get.ts
server/api/contact.post.ts
import { articleService } from '~/composables/strapi/collections/article/service' ;
export default defineEventHandler ( async ( event ) => {
const query = getQuery ( event );
const page = Number ( query . page ) || 1 ;
const pageSize = Number ( query . pageSize ) || 10 ;
try {
const result = await articleService . findMany ({
pagination: { page , pageSize },
sort: [ 'publishedAt:desc' ],
populate: [ 'author' ],
});
return result ;
} catch ( error ) {
throw createError ({
statusCode: 500 ,
message: 'Failed to fetch articles' ,
});
}
} ) ;
Composables
Create reusable composables for common data fetching patterns:
composables/useArticles.ts
import { articleService } from '~/composables/strapi/collections/article/service' ;
import type { Article } from '~/composables/strapi/collections/article/types' ;
export function useArticles ( initialPage = 1 , pageSize = 10 ) {
const page = ref ( initialPage );
const articles = ref < Article []>([]);
const pagination = ref < StrapiPagination | null >( null );
const loading = ref ( false );
const error = ref < Error | null >( null );
const fetchArticles = async () => {
loading . value = true ;
error . value = null ;
try {
const result = await articleService . findMany ({
pagination: {
page: page . value ,
pageSize ,
},
sort: [ 'publishedAt:desc' ],
populate: [ 'author' ],
});
articles . value = result . data ;
pagination . value = result . pagination ;
} catch ( e ) {
error . value = e as Error ;
} finally {
loading . value = false ;
}
};
const nextPage = () => {
if ( pagination . value && page . value < pagination . value . pageCount ) {
page . value ++ ;
}
};
const prevPage = () => {
if ( page . value > 1 ) {
page . value -- ;
}
};
// Auto-fetch on page change
watch ( page , fetchArticles );
// Initial fetch
onMounted ( fetchArticles );
return {
articles ,
pagination ,
loading ,
error ,
page ,
nextPage ,
prevPage ,
refresh: fetchArticles ,
};
}
Use the composable in your components:
< script setup lang = "ts" >
const { articles , loading , error , pagination , nextPage , prevPage } = useArticles ();
</ script >
< template >
< div >
< div v-if = " loading " > Loading... </ div >
< div v-else-if = " error " > Error: {{ error . message }} </ div >
< div v-else >
< article v-for = " article in articles " : key = " article . documentId " >
< h2 > {{ article . title }} </ h2 >
</ article >
< button @ click = " prevPage " : disabled = " pagination ?. page === 1 " >
Previous
</ button >
< button @ click = " nextPage " : disabled = " pagination ?. page === pagination ?. pageCount " >
Next
</ button >
</ div >
</ div >
</ template >
Validate forms with Zod schemas:
components/ContactForm.vue
< script setup lang = "ts" >
import { contactSchema } from '~/composables/strapi/collections/contact/schemas' ;
import type { z } from 'zod' ;
type ContactForm = z . infer < typeof contactSchema >;
const formData = reactive < ContactForm >({
name: '' ,
email: '' ,
message: '' ,
});
const errors = ref < Record < string , string >>({});
const submitting = ref ( false );
const success = ref ( false );
const handleSubmit = async () => {
errors . value = {};
submitting . value = true ;
success . value = false ;
try {
// Validate with Zod
const validated = contactSchema . parse ( formData );
// Submit to API
await $fetch ( '/api/contact' , {
method: 'POST' ,
body: validated ,
});
success . value = true ;
// Reset form
Object . assign ( formData , { name: '' , email: '' , message: '' });
} catch ( error ) {
if ( error instanceof z . ZodError ) {
error . errors . forEach (( err ) => {
if ( err . path [ 0 ]) {
errors . value [ err . path [ 0 ] as string ] = err . message ;
}
});
}
} finally {
submitting . value = false ;
}
};
</ script >
< template >
< form @ submit . prevent = " handleSubmit " >
< div >
< input v-model = " formData . name " placeholder = "Name" />
< span v-if = " errors . name " class = "error" > {{ errors . name }} </ span >
</ div >
< div >
< input v-model = " formData . email " type = "email" placeholder = "Email" />
< span v-if = " errors . email " class = "error" > {{ errors . email }} </ span >
</ div >
< div >
< textarea v-model = " formData . message " placeholder = "Message" / >
< span v-if = " errors . message " class = "error" > {{ errors . message }} </ span >
</ div >
< button type = "submit" : disabled = " submitting " >
{{ submitting ? 'Sending...' : 'Send Message' }}
</ button >
< p v-if = " success " class = "success" > Message sent successfully! </ p >
</ form >
</ template >
Service Functions Reference
All generated services include these methods:
Collection Types
findMany
findAll
findOne
findBySlug
create
update
delete
count
Fetch multiple items with pagination: const { data , pagination } = await articleService . findMany ({
filters: {
publishedAt: { $notNull: true },
},
pagination: {
page: 1 ,
pageSize: 25 ,
},
sort: [ 'publishedAt:desc' ],
populate: [ 'author' ],
locale: 'en' ,
status: 'published' ,
});
Fetch all items automatically: const articles = await articleService . findAll ({
sort: [ 'createdAt:desc' ],
});
Fetch a single item: const article = await articleService . findOne ( 'doc_123' , {
populate: [ 'author' , 'categories' ],
});
Fetch by slug: const article = await articleService . findBySlug ( 'my-article' );
Create new item: const article = await articleService . create ({
title: 'New Article' ,
content: 'Content...' ,
});
Update item: const updated = await articleService . update ( 'doc_123' , {
title: 'Updated' ,
});
Delete item: await articleService . delete ( 'doc_123' );
Count items: const total = await articleService . count ({
publishedAt: { $notNull: true },
});
Advanced Patterns
Static Site Generation
Pre-render pages at build time:
< script setup lang = "ts" >
import { articleService } from '~/composables/strapi/collections/article/service' ;
const route = useRoute ();
const slug = route . params . slug as string ;
const { data : article } = await useAsyncData ( `article- ${ slug } ` , () =>
articleService . findBySlug ( slug )
);
</ script >
Configure nuxt.config.ts for SSG:
export default defineNuxtConfig ({
nitro: {
prerender: {
crawlLinks: true ,
routes: [ '/' ],
},
} ,
}) ;
Data Caching
Cache data with cachedFunction:
import { articleService } from '~/composables/strapi/collections/article/service' ;
export const getCachedArticles = cachedFunction (
async () => {
return await articleService . findMany ({
pagination: { pageSize: 10 },
});
},
{
maxAge: 60 * 10 , // Cache for 10 minutes
name: 'articles' ,
getKey : () => 'all' ,
}
);
Parallel Requests
Fetch multiple resources in parallel:
const [ homepage , articles , categories ] = await Promise . all ([
homepageService . find (),
articleService . findMany ({ pagination: { pageSize: 5 } }),
categoryService . findAll (),
]);
Server Routes (Coming Soon)
Server Routes generation for Nuxt is in development and will be available in a future release.
When available, server routes will be automatically generated:
// Future: Auto-generated server routes
// server/api/articles/index.get.ts
// server/api/articles/[id].get.ts
// server/api/articles/index.post.ts
// etc.
Best Practices
Use useAsyncData or useFetch for SSR data fetching to benefit from automatic hydration.
Keep your STRAPI_API_TOKEN secure and only use it in server-side code (API routes, server middleware).
Create composables for common data fetching patterns to keep your code DRY.
Use watch in useAsyncData to automatically refetch when dependencies change.
Troubleshooting
Hydration Mismatch
Ensure you’re using useAsyncData or useFetch for SSR data fetching.
Type Errors
Regenerate types after schema changes:
CORS Issues
Configure CORS in your Strapi backend to allow requests from your Nuxt domain.
Next Steps