Generate a complete CRUD feature with all related components in one workflow — Model, Controller, Views, Routes, Tests, Factory, Seeder, Resource. Use when user wants to build out an entire entity at once. Triggers include "full feature", "CRUD", "resource for", "complete setup", "everything for", "build out the X feature", "scaffold everything for", or mentions an entity name with the intent of creating all components (e.g., "I need a Product entity"). For creating a single component, use th...
Installs into .claude/skills of the current project.
Are you the author of Feature?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/gurenjs-feature)
---
name: feature
description: Generate a complete CRUD feature with all related components in one workflow — Model, Controller, Views, Routes, Tests, Factory, Seeder, Resource. Use when user wants to build out an entire entity at once. Triggers include "full feature", "CRUD", "resource for", "complete setup", "everything for", "build out the X feature", "scaffold everything for", or mentions an entity name with the intent of creating all components (e.g., "I need a Product entity"). For creating a single component, use the scaffold skill instead.
---
# Feature Skill
You are a full-feature scaffolding assistant for the Guren framework.
> When filling in generated code, follow the API rules in `__RULES_DIR__/` (orm-models, controllers-http, routes-codegen, testing) — they carry the verified signatures.
## Your Role
Generate all components needed for a complete CRUD feature in one workflow. This is the "batteries-included" approach — creating everything an entity needs to work end-to-end with full type safety.
## Workflow
When given a feature name (e.g., "Post", "Product"):
### 1. Generate all components
**Preferred:** Use `make:feature` which generates type-safe code with proper imports:
```bash
bunx guren make:feature <Name> --fields "title:string,body:text,published:boolean"
```
Add `--module <name>` to scaffold the feature inside an existing `modules/<name>/` directory instead of the project root — app/ files move under `modules/<name>/`, but pages stay top-level, namespaced by module name (`resources/js/pages/<name>/...`), not colocated.
This generates Validator, Resource, Controller, Views (Index/Show/New/Edit), and Model in one step with:
- Typed page props (no `any`)
- `route()` helper for all URLs
- `ApiRoutes` for form data types
- `RouteErrors` for validation error types
**Or generate individually:**
```bash
bunx guren make:model <Name>
bunx guren make:migration create_<names>_table
bunx guren make:controller <Name>
bunx guren make:view <names>/Index
bunx guren make:view <names>/Show
bunx guren make:view <names>/New
bunx guren make:view <names>/Edit
bunx guren make:route <names>
bunx guren make:test controllers/<Name>Controller --runner=vitest
bunx guren make:factory <Name> --model=<Name>
bunx guren make:seeder <Name>
bunx guren make:resource <Name> --model=<Name>
bunx guren make:validator <Name> --fields "<name:type,...>"
```
Don't hand-write the Validator — `make:validator` emits the same file `make:feature` does, so the schema names stay in sync with what the generated controller imports. Pass the same `--fields` you would pass `make:feature`; omit it to get an empty payload schema to fill in.
The Validator is needed for both controller validation and route body schema binding.
### 2. Register routes with body schemas
When registering routes in `routes/web.ts`, **always attach Zod body schemas** to mutation routes. This enables codegen to extract types for the frontend:
```typescript
import <Name>Controller from '../app/Http/Controllers/<Name>Controller.js'
import { <Name>PayloadSchema } from '../app/Http/Validators/<Name>Validator.js'
router.group('/<names>', (<names>) => {
<names>.get('/', [<Name>Controller, 'index']).name('<names>.index')
<names>.get('/new', [<Name>Controller, 'create']).name('<names>.create')
<names>.get('/:id', [<Name>Controller, 'show']).name('<names>.show')
<names>.get('/:id/edit', [<Name>Controller, 'edit']).name('<names>.edit')
<names>.post('/', { name: '<names>.store', body: <Name>PayloadSchema }, [<Name>Controller, 'store'])
<names>.put('/:id', { name: '<names>.update', body: <Name>PayloadSchema }, [<Name>Controller, 'update'])
})
```
Where `<Name>` is PascalCase singular (e.g., `Post`) and `<names>` is kebab-case plural (e.g., `posts`).
### 3. Run codegen
```bash
bunx guren codegen
```
This generates typed route helpers and API client types in `.guren/`.
### 4. Report created files and next steps
- Add table schema to `db/schema.ts`
- Run migration: `bun run db:migrate`
## Type Safety Patterns
Generated views follow these patterns for end-to-end type safety:
### Form pages (New/Edit) — derive types from ApiRoutes
```typescript
import type { ApiRoutes } from '@/.guren/api-client.gen'
import type { RouteBody, RouteErrors } from '@guren/inertia-client/typed-forms'
import { route } from '@/.guren/routes.gen'
type <Name>FormData = RouteBody<ApiRoutes, '<names>.store'>
// Form submission uses route() helper
form.post(route('<names>.store'))
form.put(route('<names>.update', { id: <name>.id }))
```
### List/Detail pages — typed props from Resource
```typescript
import type { PaginatedPageProps } from '@guren/core'
import type { <Name>ResourceData } from '@/app/Http/Resources/<Name>Resource'
import { route } from '@/.guren/routes.gen'
interface Props extends PaginatedPageProps<<Name>ResourceData> {}
// Navigation uses route() helper
<Link href={route('<names>.show', { id: <name>.id })}>
```
### Error types — RouteErrors with message field
```typescript
interface Props {
errors?: RouteErrors<<Name>FormData> & { message?: string }
}
```
### Models — define fillable for mass assignment protection
Always add `fillable` to generated models. This is the second defense layer after Zod validation — it prevents unintended fields from reaching the database even if the controller validation is bypassed or misconfigured:
```typescript
export class <Name> extends defineModel(<names>, {
fillable: ['title', 'body', 'authorId'], // only these fields pass to create()/update()
}) {}
```
Prefer the `defineModel` option over `static fillable = [...]` — the option is typed against the table's columns, so a typo is a compile error (a `static` declaration still works and shadows the option).
For User models, credential columns (`passwordHash`, `rememberToken`) are denied from
mass assignment by `AuthenticatableModel` itself — never list them in `fillable`:
```typescript
export class User extends defineModel(users, {
base: AuthenticatableModel,
optionalOnCreate: ['passwordHash'],
requireOnCreate: ['password'],
fillable: ['name', 'email', 'password'],
}) {}
```
## Generated Structure
For feature "Post":
```
app/
├── Http/Controllers/PostController.ts
├── Http/Resources/PostResource.ts
├── Http/Validators/PostValidator.ts
└── Models/Post.ts
db/
├── factories/PostFactory.ts
├── migrations/{timestamp}_create_posts_table.sql
└── seeders/PostSeeder.ts
resources/js/pages/posts/
├── Index.tsx ← typed props, route() links
├── Show.tsx ← typed props, route() links
├── New.tsx ← ApiRoutes form type, route() submit
└── Edit.tsx ← ApiRoutes form type, RouteErrors, route() submit
tests/controllers/PostController.test.ts
```
## Schema Example
```typescript
// db/schema.ts
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
content: text('content'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
})
```
On PostgreSQL, always give timestamp columns `{ withTimezone: true }`. A
`timestamp without time zone` stores a bare wall clock, so `defaultNow()`
records it in the database session's zone while the app reads it back as UTC,
and any client other than the app sees a different instant.