Skip to main content
strapi2front uses a by-feature (“screaming architecture”) output structure. Each content type lives in its own folder with all related code co-located.

Structure Overview

strapi.config.ts
output: {
  path: "src/strapi",  // Base output directory
}
Generated structure:
src/strapi/
├── collections/           # Collection types (many entries)
│   ├── article/
│   │   ├── types.ts      # TypeScript interfaces
│   │   ├── schemas.ts    # Zod validation schemas
│   │   ├── service.ts    # API service functions
│   │   └── actions.ts    # Astro Actions
│   ├── author/
│   │   ├── types.ts
│   │   ├── schemas.ts
│   │   └── service.ts
│   └── category/
│       └── ...
├── singles/               # Single types (one entry)
│   ├── homepage/
│   │   ├── types.ts
│   │   ├── schemas.ts
│   │   └── service.ts
│   └── settings/
│       └── ...
├── components/            # Reusable components
│   ├── hero.ts           # Component type + schema
│   ├── seo.ts
│   └── cta-button.ts
└── shared/                # Shared utilities
    ├── utils.ts          # System types (media, pagination, etc.)
    ├── client.ts         # Strapi client wrapper
    ├── locales.ts        # i18n locale definitions
    ├── upload-client.ts  # Browser upload helper (if enabled)
    └── upload-action.ts  # Server upload action (if enabled)

Collections vs Singles

Content types that have multiple entries (articles, products, users, etc.)Location: collections/{singular-name}/Files:
  • types.ts - Type definitions + filters
  • schemas.ts - Create/update validation
  • service.ts - Full CRUD operations
  • actions.ts - Astro action wrappers
Example:
collections/
  article/
    types.ts      → Article, ArticleFilters
    schemas.ts    → articleCreateSchema, articleUpdateSchema
    service.ts    → articleService (findMany, findOne, create, etc.)
    actions.ts    → article actions

File Naming

All folder and file names use kebab-case derived from your Strapi content type names:
Strapi Content TypeFolder NameImport Path
Articlearticle/collections/article/
BlogPostblog-post/collections/blog-post/
ProductCategoryproduct-category/collections/product-category/
SEO (component)seo.tscomponents/seo
CTAButton (component)cta-button.tscomponents/cta-button
The converter uses singularName from Strapi (e.g., “article”, “blogPost”) and converts to kebab-case.

Import Patterns

strapi2front does NOT generate barrel exports (index.ts). Always import from specific files:
import { articleService } from '@/strapi/collections/article/service';
import type { Article } from '@/strapi/collections/article/types';
import { articleCreateSchema } from '@/strapi/collections/article/schemas';

Multiple Imports from Same Feature

// Import multiple items from same file
import type { 
  Article, 
  ArticleFilters,
  ArticleCreateInput,
  ArticleUpdateInput 
} from '@/strapi/collections/article/types';

// Import from different files
import { articleService } from '@/strapi/collections/article/service';
import { articleCreateSchema } from '@/strapi/collections/article/schemas';
import type { Article } from '@/strapi/collections/article/types';

Generated File Contents

Types File

/**
 * Article
 * News and blog articles
 * Generated by strapi2front
 */

import type { StrapiBaseEntity, StrapiMedia, BlocksContent } from '../../shared/utils';
import type { Author } from '../../collections/author/types';
import type { Tag } from '../tag/types';
import type { Seo } from '../../components/seo';

export interface Article extends StrapiBaseEntity {
  title: string;
  slug: string;
  content: BlocksContent;
  excerpt?: string;
  cover: StrapiMedia | null;
  author: Author | null;
  tags: Tag[];
  seo: Seo | null;
  publishedDate: string;
}

export interface ArticleFilters {
  id?: number | { $eq?: number; $ne?: number; $in?: number[]; $notIn?: number[] };
  documentId?: string | { $eq?: string; $ne?: string };
  title?: string | { $contains?: string; $startsWith?: string; $endsWith?: string };
  slug?: string | { $eq?: string };
  createdAt?: string | { $gt?: string; $gte?: string; $lt?: string; $lte?: string };
  updatedAt?: string | { $gt?: string; $gte?: string; $lt?: string; $lte?: string };
  publishedAt?: string | null | { $eq?: string; $ne?: string; $null?: boolean };
  $and?: ArticleFilters[];
  $or?: ArticleFilters[];
  $not?: ArticleFilters;
}
Includes:
  • Interface extending StrapiBaseEntity
  • Relations imported from other types
  • Filter interface for querying (collections only)
/**
 * Article Schemas
 * Generated by strapi2front
 */

import { z } from 'zod';
import { seoSchema } from '../../components/seo';

/**
 * Zod schema for creating articles
 */
export const articleCreateSchema = z.object({
  title: z.string(),
  slug: z.string(),
  content: z.array(z.any()),  // BlocksContent
  excerpt: z.string().optional(),
  cover: z.number().optional(),
  author: z.string().optional(),  // documentId
  tags: z.array(z.string()).optional(),
  seo: seoSchema.optional(),
  publishedDate: z.string().optional(),
});

/**
 * Zod schema for updating articles (all fields optional)
 */
export const articleUpdateSchema = z.object({
  title: z.string().optional(),
  slug: z.string().optional(),
  content: z.array(z.any()).optional(),
  excerpt: z.string().optional(),
  cover: z.number().optional(),
  author: z.string().optional(),
  tags: z.array(z.string()).optional(),
  seo: seoSchema.optional(),
  publishedDate: z.string().optional(),
});

export type ArticleCreateInput = z.infer<typeof articleCreateSchema>;
export type ArticleUpdateInput = z.infer<typeof articleUpdateSchema>;
Includes:
  • Create schema (required fields enforced)
  • Update schema (all fields optional)
  • TypeScript types inferred from schemas
See Features > Services for complete service API.Exports:
  • {name}Service object with methods:
    • findMany() - Paginated list
    • findAll() - All entries (auto-paginated)
    • findOne() - By documentId/id
    • findBySlug() - By slug (if exists)
    • create() - Create new
    • update() - Update existing
    • delete() - Delete
    • count() - Count with filters
/**
 * Article Actions (Astro)
 * Generated by strapi2front
 */

import { defineAction } from 'astro:actions';
import { z } from 'zod';
import { articleService } from './service';
import { articleCreateSchema, articleUpdateSchema } from './schemas';

const findManySchema = z.object({
  filters: z.any().optional(),
  pagination: z.object({
    page: z.number().optional(),
    pageSize: z.number().optional(),
  }).optional(),
  populate: z.any().optional(),
});

export const article = {
  findMany: defineAction({
    input: findManySchema,
    handler: async (input) => {
      return articleService.findMany(input);
    },
  }),
  
  findOne: defineAction({
    input: z.object({ documentId: z.string() }),
    handler: async ({ documentId }) => {
      return articleService.findOne(documentId);
    },
  }),
  
  create: defineAction({
    input: articleCreateSchema,
    handler: async (input) => {
      return articleService.create(input);
    },
  }),
  
  // ... update, delete actions
};

Components

Components generate a single file with type + schema:
/**
 * SEO component
 * Category: shared
 * Generated by strapi2front
 */

import { z } from 'zod';
import type { StrapiMedia } from '../shared/utils';

export interface Seo {
  id: number;
  metaTitle: string;
  metaDescription: string;
  metaImage: StrapiMedia | null;
  keywords?: string;
}

/**
 * Zod schema for SEO component
 */
export const seoSchema = z.object({
  metaTitle: z.string(),
  metaDescription: z.string(),
  metaImage: z.number().optional(),
  keywords: z.string().optional(),
});

export type SeoInput = z.infer<typeof seoSchema>;

Shared Files

System types used across all generated code:
/**
 * Strapi utility types
 * Generated by strapi2front
 * Strapi version: v5
 */

export interface StrapiBaseEntity {
  id: number;
  documentId: string;  // v5 only
  createdAt: string;
  updatedAt: string;
  publishedAt: string | null;
}

export interface StrapiMedia {
  id: number;
  documentId: string;
  name: string;
  alternativeText: string | null;
  caption: string | null;
  width: number;
  height: number;
  url: string;
  formats: { /* ... */ } | null;
  // ...
}

export interface StrapiPagination {
  page: number;
  pageSize: number;
  pageCount: number;
  total: number;
}

export interface StrapiResponse<T> { /* ... */ }
export interface StrapiListResponse<T> { /* ... */ }
export interface StrapiFileInfo { /* ... */ }

// Rich text content
export type BlocksContent = unknown[];  // or from @strapi/blocks-react-renderer
Wrapper around @strapi/client with type safety:
import { strapi } from '@strapi/client';

export interface ClientOptions {
  authToken?: string;
  baseURL?: string;
}

export function createStrapiClient(options?: ClientOptions) { /* ... */ }
export const strapiClient = createStrapiClient();

export function collection<T>(pluralName: string, clientOptions?: ClientOptions) { /* ... */ }
export function single<T>(singularName: string, clientOptions?: ClientOptions) { /* ... */ }

export const files = {
  upload: async (file: File, options?) => { /* ... */ },
  find: async (params?) => { /* ... */ },
  // ...
};
i18n locale definitions from your Strapi instance:
/**
 * Strapi locales
 * Generated by strapi2front
 */

export const locales = ['en', 'es', 'fr'] as const;
export type Locale = typeof locales[number];

export const defaultLocale: Locale = 'en';

export const localeNames: Record<Locale, string> = {
  'en': 'English',
  'es': 'Español',
  'fr': 'Français',
};

export function isValidLocale(code: string): code is Locale {
  return locales.includes(code as Locale);
}

export function getLocaleName(code: Locale): string {
  return localeNames[code] || code;
}

Path Alias Setup

For cleaner imports, configure a path alias in your project:
tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/strapi/*": ["./src/strapi/*"]
    }
  }
}
Usage:
import { articleService } from '@/strapi/collections/article/service';

Why By-Feature?

Projects can grow to hundreds of content types without becoming unwieldy. Each content type is isolated in its own folder.
collections/
  article/     ← 4 files
  author/      ← 4 files
  category/    ← 4 files
  ...          ← 100+ more, easy to navigate
Everything related to a content type lives together. Want to see all article-related code? Look in collections/article/.No hunting across types/, services/, schemas/ folders.
Import paths clearly show relationships:
// Article depends on Author and Tag
import type { Author } from '../../collections/author/types';
import type { Tag } from '../tag/types';
Renaming or removing a content type? Just delete/rename its folder. All related code is co-located.

Alternative Structures

strapi2front currently only supports by-feature structure. Future versions may add:
  • By-type structure - Group by file type (types/, services/, etc.)
  • Flat structure - All files in one directory
  • Custom templates - Define your own structure
Interested in other structures? Open an issue to discuss!

Output Example

With this Strapi schema:
  • Collections: Article, Author, Category
  • Singles: Homepage
  • Components: SEO, Hero
You get:
src/strapi/
├── collections/
│   ├── article/
│   │   ├── types.ts      (Article, ArticleFilters)
│   │   ├── schemas.ts    (articleCreateSchema, articleUpdateSchema)
│   │   ├── service.ts    (articleService)
│   │   └── actions.ts    (article actions)
│   ├── author/
│   │   ├── types.ts
│   │   ├── schemas.ts
│   │   ├── service.ts
│   │   └── actions.ts
│   └── category/
│       └── ...
├── singles/
│   └── homepage/
│       ├── types.ts      (Homepage)
│       ├── schemas.ts    (homepageUpdateSchema)
│       └── service.ts    (homepageService)
├── components/
│   ├── seo.ts            (Seo, seoSchema)
│   └── hero.ts           (Hero, heroSchema)
└── shared/
    ├── utils.ts
    ├── client.ts
    ├── locales.ts
    └── upload-client.ts  (if enabled)

Next Steps