Codync
무료 오픈소스 1:1 대안, Grok Bot과 Muse 대신
CleanSlice full-stack starter kit — NestJS + Nuxt with Clean Architecture vertical slices, Prisma, Pinia, shadcn-vue, Tailwind.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Full-stack starter kit built on the CleanSlice architecture — vertical slices with Clean Architecture layers, using NestJS (backend) and Nuxt (frontend).
| Layer | Technology |
|---|---|
| Backend | NestJS, Prisma, PostgreSQL |
| Frontend | Nuxt 3, Vue 3, Pinia |
| Styling | Tailwind CSS, shadcn-vue |
| API Contract | Swagger/OpenAPI, @hey-api/openapi-ts |
| Infrastructure | Docker Compose |
nest-nuxt-starter-kit/
├── api/ # Backend (NestJS)
│ ├── src/
│ │ ├── slices/
│ │ │ ├── setup/
│ │ │ │ ├── prisma/ # Database ORM (global)
│ │ │ │ ├── error/ # Error handling interceptors
│ │ │ │ ├── health/ # Health check endpoint
│ │ │ │ ├── id/ # Primary IDs: formatId(), withIdRetry(), @IsEntityId()
│ │ │ │ └── mail/ # Email sending (SMTP; Mailpit locally)
│ │ │ └── user/ # Example feature slice
│ │ │ ├── domain/ # Types, abstract gateway
│ │ │ ├── data/ # Concrete gateway, mapper
│ │ │ └── dtos/ # Request/response DTOs
│ │ ├── app.module.ts
│ │ └── main.ts
│ ├── prisma/
│ │ └── schema.prisma # Auto-generated (do not edit)
│ ├── scripts/
│ │ └── cleanslice-check.cjs # Boundary check (runs on `bun run dev`)
│ ├── cleanslice.config.cjs # Slice groups, lowest first
│ ├── docker-compose.yml
│ ├── .env.example
│ └── tsconfig.json
│
├── app/ # Frontend (Nuxt)
│ ├── slices/
│ │ ├── setup/
│ │ │ ├── theme/ # Tailwind + shadcn-vue components
│ │ │ ├── pinia/ # State management config
│ │ │ ├── api/ # Generated API SDK (@hey-api)
│ │ │ ├── error/ # Error handling + toasts
│ │ │ └── i18n/ # Internationalization
│ │ └── user/ # Example feature slice
│ │ ├── pages/
│ │ ├── components/
│ │ └── stores/
│ ├── nuxt.config.ts
│ ├── registerSlices.ts # Auto-discovers slice layers
│ └── app.vue
│
└── README.md
.nvmrc) — still the production runtime for the apiThis starter kit includes a /setup skill that installs the required agent skills for AI-assisted development:
| Skill | Purpose |
|---|---|
| cleanslice | Architecture patterns, vertical slices, gateway, Provider.vue |
| shadcn-vue | UI component library, Reka UI, Tailwind, dark mode |
| conventional-commits | Conventional Commits standard for git messages |
Run /setup in Claude Code to install all skills. You can also install them manually:
bunx playbooks add skill noartem/skills --skill shadcn-vue
bunx skills add CleanSlice/skills --skill cleanslice
bunx skills add CleanSlice/skills --skill conventional-commits
Restart the Claude Code session after installing to activate the skills.
# 1. Clone the starter kit
git clone <repo-url> my-project
cd my-project
# 2. Start the API
cd api
cp .env.example .env.dev
bun install
docker compose up -d # Starts PostgreSQL + Mailpit (inbox: http://localhost:4232)
bun run migrate # Runs Prisma migrations
bun run dev # http://localhost:3000
# Swagger UI: http://localhost:3000/api
# 3. Start the App (new terminal)
cd app
bun install
bun run build:api # Generates typed SDK from Swagger spec
bun run dev # http://localhost:3001
# 4. Install AI development skills (Claude Code)
/setup # Installs CleanSlice + shadcn-vue skills
Every feature is a self-contained slice — a folder that owns its own layers, routes, components, and data access. Adding a feature means adding a folder. Removing one means deleting it.
slices/user/ # One slice = one feature
├── user.controller.ts # Presentation layer
├── domain/ # Domain layer (business rules)
│ ├── user.types.ts # Interfaces: IUserData
│ └── user.gateway.ts # Abstract class: IUserGateway
├── data/ # Data layer (infrastructure)
│ ├── user.gateway.ts # Concrete gateway (Prisma)
│ └── user.mapper.ts # Prisma → domain conversion
└── dtos/ # API boundary
├── user.dto.ts # Response DTO
└── createUser.dto.ts # Request DTO
Presentation → Domain → Data
(HTTP/UI) (Rules) (Prisma/API)
Dependencies always point inward. The domain layer has no knowledge of frameworks, databases, or UI.
Those rules are invisible to the compiler — TypeScript accepts a controller that injects a gateway from a neighbouring slice — so they are checked by a program instead of by review:
bun run check # also runs automatically before `bun run dev`
cleanslice-check: OK — 2 group(s) [setup -> user], 64 modules, 296 ms
Three rules: groups depend only downward, no cycles anywhere, and inside a
slice the presentation and domain layers never touch data/. The group order
is declared in api/cleanslice.config.cjs, so the script itself is the same
in every CleanSlice project:
module.exports = {
groups: ['setup', 'user'], // lowest first — the order IS the rule
};
What the check does NOT catch is written down: CleanSlice MCP,
02-standards/boundary-check.md. A green run means "none of these three rules
is broken", never "this code is CleanSlice".
The abstract gateway lives in domain/, the concrete implementation in data/. Prisma IS the repository — no extra layer needed.
// domain/user.gateway.ts — the contract
export abstract class IUserGateway {
abstract findAll(): Promise<IUserData[]>;
abstract findById(id: string): Promise<IUserData | null>;
abstract create(data: ICreateUserData): Promise<IUserData>;
}
// data/user.gateway.ts — the implementation
@Injectable()
export class UserGateway extends IUserGateway {
constructor(
private readonly prisma: PrismaService,
private readonly mapper: UserMapper,
) { super(); }
async findById(id: string): Promise<IUserData | null> {
const user = await this.prisma.user.findUnique({ where: { id } });
return user ? this.mapper.toDomain(user) : null;
}
}
Every component folder has a Provider.vue that handles data fetching. Child components receive data via props:
<!-- components/user/Provider.vue -->
<script setup lang="ts">
const props = defineProps<{ id: string }>();
const store = useUserStore();
const { data, pending } = await useAsyncData('user', () => store.fetchById(props.id));
</script>
<template>
<UserItem v-if="!pending" :user="data" />
<div v-else>Loading...</div>
</template>
| What | Rule | Example |
|---|---|---|
| Slice folders | Singular | user/, project/, chat/ |
| Controller routes | Plural | @Controller('users') |
| Page files | Plural | pages/users.vue |
| DTO files | camelCase | createUser.dto.ts |
| Interfaces | I prefix | IUserData, IUserGateway |
| Enums | Types suffix | UserStatusTypes, RoleTypes |
| Enum values | String | Active = 'active' |
Setup slices provide shared infrastructure. They are pre-configured in this starter kit.
api/src/slices/setup/)| Slice | Purpose |
|---|---|
prisma/ | Database ORM — global PrismaService |
error/ | Error handling + response interceptors |
health/ | GET /health endpoint |
id/ | Primary IDs: formatId(prefix) + withIdRetry() in gateways, @IsEntityId() / EntityIdPipe at the boundary |
mail/ | Email sending behind IMailGateway; Mailpit locally |
app/slices/setup/)| Slice | Purpose | Alias |
|---|---|---|
theme/ | Tailwind CSS + shadcn-vue components | #theme |
pinia/ | State management + store auto-discovery | — |
api/ | Generated TypeScript SDK from Swagger | #api |
error/ | Error handling + toast notifications | #error |
i18n/ | Internationalization (en, fr, etc.) | #i18n |
Registration order matters — dependencies load first. This is handled by registerSlices.ts.
mkdir -p api/src/slices/project/{domain,data,dtos}
Create the files:
api/src/slices/project/
├── project.module.ts
├── project.controller.ts # @Controller('projects')
├── domain/
│ ├── index.ts
│ ├── project.types.ts # IProjectData, ICreateProjectData
│ └── project.gateway.ts # Abstract IProjectGateway
├── data/
│ ├── project.gateway.ts # Concrete gateway (Prisma)
│ └── project.mapper.ts
└── dtos/
├── index.ts
├── project.dto.ts
├── createProject.dto.ts
└── filterProject.dto.ts
Add the Prisma model at the slice root:
// api/src/slices/project/project.prisma
model Project {
id String @id // minted by the gateway: formatId('project'), never @default(uuid()/cuid())
name String
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Register in app.module.ts:
import { ProjectModule } from './slices/project/project.module';
@Module({
imports: [
// ... setup slices
ProjectModule,
],
})
export class AppModule {}
Run migration and regenerate:
bun run migrate # Merges schemas + creates migration
bun run dev # Restart to update swagger-spec.json
mkdir -p app/slices/project/{pages,components/project,components/projectList,stores,locales}
Create the files:
app/slices/project/
├── nuxt.config.ts # Alias: #project
├── pages/
│ ├── projects.vue # /projects
│ └── projects/[id].vue # /projects/:id
├── components/
│ ├── project/
│ │ ├── Provider.vue # Data fetching
│ │ ├── Item.vue # Display
│ │ └── Form.vue # Create/edit
│ └── projectList/
│ ├── Provider.vue
│ └── Thumb.vue # List card
├── stores/
│ └── project.ts # Pinia store
└── locales/
└── en.json
Regenerate the API SDK:
bun run build:api # Generates typed services from Swagger
The slice auto-registers via registerSlices.ts — no manual config needed.
# prefix via tsconfig paths)import { PrismaService } from '#/setup/prisma';
import { IUserData } from '#/user/domain';
# prefix via nuxt.config alias)import { UsersService, type UserDto } from '#api/data';
import { cn } from '#theme/utils/cn';
Nuxt auto-imports Vue APIs, composables, components, and Pinia stores — no manual imports needed for those.
Every command here runs through Bun — bun install, bun run <script>,
bunx <cli>. The lockfile is bun.lock; there is no package-lock.json in
this repo and adding one back would give the project two disagreeing
dependency graphs.
| Script | Purpose |
|---|---|
bun run dev | Start dev server with hot reload (runs check first) |
bun run check | CleanSlice boundary check — groups, cycles, slice layers |
bun run docker | Start Docker services (PostgreSQL, Mailpit) |
bun run migrate | Merge Prisma schemas + run migrations |
bun run generate | Merge Prisma schemas only |
bun run studio | Open Prisma Studio (DB browser) |
bun run build | Production build |
| Script | Purpose |
|---|---|
bun run dev | Start Nuxt dev server |
bun run build:api | Regenerate API SDK from Swagger spec |
bun run build | Production build (regenerates SDK first) |
Copy .env.example to .env.dev in the api/ directory:
# Database (Docker)
DATABASE_URL=postgresql://postgres:root@localhost:5432/starter-api-local-database
# Server
PORT=3000
CORS_ORIGIN=http://localhost:3001
# JWT
JWT_SECRET=change-me-in-production
JWT_EXPIRES_IN=7d
# Email (Docker: Mailpit catches everything, inbox at http://localhost:4232)
SMTP_HOST=localhost
SMTP_PORT=4231
SMTP_SECURE=false
SMTP_FROM=CleanSlice <[email protected]>
The image reads these from the environment, and the api exits on boot without them:
| Variable | Read by | Consequence when missing |
|---|---|---|
DATABASE_URL | Prisma, both at start.sh migration time and at runtime | migrations fail, api never starts |
JWT_SECRET | JwtStrategy via getOrThrow | Configuration key "JWT_SECRET" does not exist — api exits |
A missing JWT_SECRET is quiet from the outside. Nuxt keeps serving pages on
/, and only /health and /api/* return 502 — nginx is up, the api behind
it is not.
slices/ — no root-level components/, pages/, or services/<prefix>-<adjective>-<noun>-<12 Crockford>, minted by the gateway with formatId(); never @default(uuid()) / cuid()IUserGateway is both the type and the injection tokenstores/, not composables/any type — use unknown with type guardsMIT