Back to skills
SKILL.md
Guren Api
ASecurityHuman-browsable Guren API reference — code patterns and examples for every subsystem (Controllers, Models, Routes, Middleware, Authentication, Authorization, Events, Jobs, Queue, Mail, Cache, Validation, Broadcasting, Notifications, Storage, Scheduling, I18n, Encryption, Health Checks, Error Handling, Container/ServiceProvider, Console Commands, API Resources). Use when the user asks "how to", "how does", "example of", or "what is" about a Guren API. Agent-critical signatures are already avai...
- 30 stars
- 0 votes
- 0 copies
- 1 view
- Added September 23, 2026
Works with
Security analysis
100/100npx -y skills add gurenjs/guren --skill guren-api --agent claude-codeAre you the author of Guren Api?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/gurenjs-guren-api)---
name: guren-api
description: Human-browsable Guren API reference — code patterns and examples for every subsystem (Controllers, Models, Routes, Middleware, Authentication, Authorization, Events, Jobs, Queue, Mail, Cache, Validation, Broadcasting, Notifications, Storage, Scheduling, I18n, Encryption, Health Checks, Error Handling, Container/ServiceProvider, Console Commands, API Resources). Use when the user asks "how to", "how does", "example of", or "what is" about a Guren API. Agent-critical signatures are already available via the guren context digest and __RULES_DIR__/ — consult those first during implementation.
---
# Guren API Documentation Skill
You are a documentation assistant for the Guren framework.
> The authoritative signature-level API reference lives in `__RULES_DIR__/*.md` (each file's `paths` frontmatter states the paths it covers); this skill is a subsystem tour for interactive Q&A.
## Your Role
Help users understand and use Guren framework APIs by providing examples, patterns, and source file locations.
## Core Subsystems
### Controllers
Source: `packages/server/src/mvc/Controller.ts`
```typescript
import { Controller } from '@guren/core'
import { pages } from '@/.guren/pages.gen'
import { PostPayloadSchema, PostIdParamSchema } from '../Validators/PostValidator.js'
export default class PostController extends Controller {
async index() {
const posts = await Post.all()
return this.inertia(pages.posts.Index, { posts })
}
async show() {
const { id } = this.validateParams(PostIdParamSchema) // throws 422
const post = await Post.findOrFail(id) // throws 404
return this.inertia(pages.posts.Show, { post })
}
async store() {
// routes/web.ts: router.post('/posts', { name: 'posts.store', body: PostPayloadSchema }, [PostController, 'store'])
const { body: data } = this.validated('posts.store') // route contract already answered 422
const user = await this.auth.userOrFail<UserRecord>() // throws 401 — <T> defaults to Authenticatable, no .id
const post = await Post.create({ ...data, authorId: user.id })
return this.redirect('/posts/' + post?.id)
}
}
```
> **Note:** Server-side controllers use URL string literals for redirects. The `route()` helper is a frontend-only utility generated by codegen for React components.
For public content pages that need no client framework (blog posts, docs),
`this.view(Component, props, options?)` renders a `hono/jsx` component from
`app/View/` to plain SSR HTML — see `rules/controllers-http.md`
("Server-rendered content pages") for the conventions.
**Route contract input**: a route's `params`/`query`/`body` schemas are validated before the action (422 on failure):
- `this.validated('route.name')` → `{ params, query, body }` as the schemas parsed them; typed after `bunx guren codegen`,
an undeclared segment is `undefined`, and a name other than the served route throws
**Validation helpers** (Zod duck-type — any schema with `safeParse()` works), for routes without a contract:
- `this.validateBody<T>(schema): Promise<T>` — parse request body
- `this.validateQuery<T>(schema): T` — parse query parameters
- `this.validateParams<T>(schema): T` — parse route parameters
All throw `ValidationException` (HTTP 422) on failure.
**Auth helpers:**
- `this.auth.user<T>()` — returns user or null
- `this.auth.userOrFail<T>()` — returns user or throws `AuthenticationException` (401)
### Models
Source: `packages/orm/src/Model.ts`
```typescript
import { defineModel } from '@guren/core'
import { posts } from '@/db/schema'
export class Post extends defineModel(posts) {}
// Usage
await Post.find(1) // returns null if not found
await Post.findOrFail(1) // throws ModelNotFoundException (404)
await Post.where('published', true).get()
await Post.create({ title: 'Hello' })
```
`ModelNotFoundException` (source: `packages/orm/src/ModelNotFoundException.ts`) carries `statusCode: 404` and is automatically rendered as HTTP 404 by the ExceptionHandler.
**Mass assignment protection** — prevent injection of unintended fields via `create()` / `update()`:
```typescript
export class Post extends defineModel(posts, {
// Whitelist: only these fields are accepted (recommended).
// Typed against the table's columns — a typo is a compile error.
fillable: ['title', 'excerpt', 'body', 'authorId'],
}) {}
export class User extends defineModel(users, {
base: AuthenticatableModel,
// The model hashes a plain `password` into `passwordHash`
optionalOnCreate: ['passwordHash'],
requireOnCreate: ['password'],
// Whitelist for mass assignment (credential columns are denied by the base class)
fillable: ['name', 'email', 'password'],
}) {}
```
- `fillable` (whitelist) — only listed fields pass through to `create()` / `update()`; unlisted input keys **throw `MassAssignmentException`**. The primary key (`id`) is always silently stripped. `static fillable = [...]` on the class also works (untyped) and shadows the option; `hidden`, `visible`, `accessors`, and `appends` have the same typed option forms.
- Credential columns (`passwordHash`, `rememberToken`) **always throw** on authenticatable models — the framework denies them; listing them in `fillable` does not open them
- Enforced by `Model.filterFillable()`, called automatically before persistence
- Always define `fillable` on models that accept user input — this is the second defense layer after Zod validation
- `forceCreate()` / `forceUpdate()` bypass filtering for trusted server-side values (e.g. `passwordHash: 'oauth:...'`). **Never call them with request input**
**Relationships** — declare once, eager-load anywhere:
```typescript
// app/Models/User.ts
export class User extends defineModel(users, { base: AuthenticatableModel, optionalOnCreate: ['passwordHash'] }) {
static override relationTypes: { posts: HasManyRecord<PostRecord> } = { posts: [] }
}
User.hasMany('posts', () => import('./Post.js').then((m) => m.Post), 'authorId', 'id')
// app/Models/Post.ts
export class Post extends defineModel(posts) {
static override relationTypes: { author: BelongsToRecord<UserRecord> } = { author: null }
}
Post.belongsTo('author', () => import('./User.js').then((m) => m.User), 'authorId', 'id')
// Eager loading (typed via relationTypes)
await User.with('posts') // users[0].posts: PostRecord[]
await User.with('posts.comments') // nested, dot notation
await User.where('active', true).with('posts').get() // QueryBuilder
await User.findWith(1, 'posts') // single record + relations
await User.withCount('posts') // users[0].postsCount: number (no rows loaded)
await Post.withPaginate('author', { page: 1 }) // paginated + relations
```
Also available: `hasOne`, `belongsToMany(name, related, pivotTable, foreignPivotKey, relatedPivotKey, parentKey?, relatedKey?)`, `hasManyThrough`, `morphMany`/`morphTo`. There are **no `attach`/`detach`/`sync` pivot helpers and no `firstOrCreate`/`updateOrCreate`** — manage pivot rows via a model on the pivot table (`PivotModel.create(...)` / `PivotModel.delete(...)`), and hand-roll find-or-create with `Model.first(where)` + `Model.create(...)`. Full guide: `docs/en/guides/database.md` (Relationships section).
### Routes
Source: `packages/server/src/mvc/Router.ts`
```typescript
import { Router, requireAuthenticated } from '@guren/core'
import { PostPayloadSchema } from '../app/Http/Validators/PostValidator.js'
export function registerWebRoutes(baseRouter: Router): void {
// aliasMiddleware() returns a Router carrying the alias name in its type —
// capture it, or a later .middleware('auth') will not compile.
const router = baseRouter.aliasMiddleware('auth', requireAuthenticated({ redirectTo: '/login' }))
router.get('/posts', [PostController, 'index']).name('posts.index')
// Attach body schema to mutation routes for codegen type extraction
router.post('/posts', { name: 'posts.store', body: PostPayloadSchema }, [PostController, 'store'])
router.put('/posts/:id', { name: 'posts.update', body: PostPayloadSchema }, [PostController, 'update'])
router.middleware('auth').group((auth) => {
auth.get('/dashboard', [DashboardController, 'index'])
})
}
```
**Verbs**: `get / post / put / patch / delete / query` share the same overloads;
`router.on(method, path, ...)` covers anything else. `query()` registers HTTP QUERY
(RFC 10008) — safe like GET but body-carrying; read-only handlers only (CSRF skips it),
not expressible in OpenAPI 3.1, not sendable from Inertia forms (use the API client).
**Route contract options** — attach `body`, `params`, `query` schemas to routes:
- Schemas are metadata for codegen (not double-validated for Controller actions)
- `bunx guren codegen` extracts schemas → generates typed `ApiRoutes` interface
- Frontend derives form types via `RouteBody<ApiRoutes, 'route.name'>`
### End-to-End Type Safety
Source: `packages/cli/src/api-client-types.ts`, `packages/inertia-client/src/typed-forms.ts`
Guren provides bidirectional type safety between frontend forms and backend validation:
```
Zod schema (Validator) → Route body option → codegen → ApiRoutes → Frontend form type
→ Route contract middleware → Runtime validation (422 on failure) → this.validated()
```
`ApiRoutes[...]['body']` is the **request** shape — what the browser sends, before
validation. A coercing schema is rendered as it travels: `z.coerce.date()` is a
`string` in the form and a `Date` in the controller. `['response']` is the other
side, the parsed shape a client gets back.
**1. Define schema once** (Validator file):
```typescript
export const PostPayloadSchema = z.object({ title: z.string().min(1), body: z.string().min(1) })
```
**2. Attach to route** (routes/web.ts):
```typescript
router.post('/posts', { name: 'posts.store', body: PostPayloadSchema }, [PostController, 'store'])
```
**3. Frontend derives types** (after `bunx guren codegen`):
```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 PostFormData = RouteBody<ApiRoutes, 'posts.store'> // { title: string; body: string }
type PostErrors = RouteErrors<PostFormData> // Partial<Record<'title' | 'body', string | string[]>>
// Typed form submission
form.post(route('posts.store'))
// Typed navigation
<Link href={route('posts.show', { id: post.id })}>
```
**4. Controller reads the validated body** (the route validated it before the action ran):
```typescript
const { body: data } = this.validated('posts.store') // typed from the contract after codegen
```
### Middleware
Source: `packages/server/src/http/middleware/`
```typescript
import { defineMiddleware } from '@guren/core'
export const logRequest = defineMiddleware(async (ctx, next) => {
console.log(ctx.req.method, ctx.req.url)
await next()
})
```
### Authentication
Source: `packages/server/src/auth/`
```typescript
import { Router, requireAuthenticated } from '@guren/core'
const router = new Router()
.aliasMiddleware('auth', requireAuthenticated({ redirectTo: '/login' }))
router.middleware('auth').group((auth) => {
// Protected routes
})
// In controller
const user = await this.auth.user() // returns user | null
const user = await this.auth.userOrFail() // throws AuthenticationException (401)
const isLoggedIn = await this.auth.check() // returns boolean
```
Additional auth features:
- API Tokens: `packages/server/src/auth/api-token.ts`
- Email Verification: `packages/server/src/auth/email-verification.ts`
- Password Reset: `packages/server/src/auth/password-reset.ts`
### Testing (@guren/testing)
Source: `packages/testing/src/`
Included by default in `create-app`-scaffolded apps' `devDependencies`; if missing, `bun add -d @guren/testing`.
```typescript
import { TestApp } from '@guren/testing'
const app = await TestApp.create({
auth: {}, // mounts session + CSRF middleware (needed for withCsrf())
routes: registerWebRoutes,
})
await app.get('/posts').assertOk()
const csrf = await app.actingAs(user).withCsrf() // header auth + primed XSRF cookie
await csrf.post('/posts', { title: 'Hi' }).assertRedirect('/posts')
```
- `actingAs(user)` / `withCsrf()` each return a **new** `TestApp` — chain or reassign, don't discard the result
- Without `auth`, no session/CSRF middleware is mounted — `app.json().post(...)` skips CSRF entirely, fine for quick checks but not production-representative
- The main entry has no vitest dependency; DB lifecycle helpers (`useDatabaseTransactions` etc.) use bun:test globals automatically — vitest projects `import '@guren/testing/vitest'` once in setup to register hooks
## Extended Subsystems
### Authorization (Gate & Policy)
Source: `packages/server/src/authorization/`
- `Gate.ts` — Define abilities and policies
- `Policy.ts` — Resource-based authorization
- `middleware.ts` — Route-level authorization middleware
### Events & Listeners
Source: `packages/server/src/events/`
- `Event.ts` — Base event class
- `EventManager.ts` — Event dispatcher
- `Listener.ts` — Base listener class
- `builtin.ts` — Built-in framework events
Register listeners with `events.listen(Listener)` in the app's event provider
(`app/Providers/EventProvider.ts`), and list that provider in `createApp({ providers })`.
### Jobs & Queue
Source: `packages/server/src/queue/`
- `Job.ts` — Base job class with `handle()` method
- `QueueManager.ts` — Queue manager (memory, Redis drivers)
- `Worker.ts` — Queue worker process
Drivers: `packages/server/src/queue/drivers/`
### Mail
Source: `packages/server/src/mail/`
- `Mail.ts` — Base mailable class
- `MailManager.ts` — Mail manager
Transports:
- `MemoryTransport.ts` — For testing
- `ResendTransport.ts` — Resend API
- `SmtpTransport.ts` — SMTP
### Cache
Source: `packages/server/src/cache/`
- `CacheManager.ts` — Cache manager
- `TaggedCache.ts` — Tag-based cache invalidation
Stores:
- `MemoryStore.ts` — In-memory
- `FileStore.ts` — File-based
- `RedisStore.ts` — Redis
### Validation
Source: `packages/server/src/http/validation/`
- `Validator.ts` — Validation engine
- `rules.ts` — Built-in validation rules
- `FormRequest.ts` — Form request validation (`packages/server/src/http/FormRequest.ts`)
### Broadcasting
Source: `packages/server/src/broadcasting/`
- `BroadcastManager.ts` — Broadcast manager
Channels: `Channel.ts`, `PrivateChannel.ts`, `PresenceChannel.ts`
Drivers: `MemoryDriver.ts`, `RedisDriver.ts`
### Notifications
Source: `packages/server/src/notifications/`
- `Notification.ts` — Base notification class
- `NotificationManager.ts` — Notification dispatcher
Channels: `MailChannel.ts`, `SlackChannel.ts`, `DatabaseChannel.ts`, `MemoryChannel.ts`
### Storage
Source: `packages/server/src/storage/`
- `StorageManager.ts` — Storage manager
Drivers: `LocalDriver.ts`, `MemoryDriver.ts`, `S3Driver.ts`. On Cloudflare Workers, `R2Driver` from `@guren/plugin-cloudflare` (bucket binding; register with `storage.registerDisk()`).
### Scheduling
Source: `packages/server/src/scheduling/`
- `Schedule.ts` — Schedule definition
- `Scheduler.ts` — Scheduler runner
- `ScheduledTask.ts` — Individual scheduled task
- `CronParser.ts` — Cron expression parser
### I18n (Internationalization)
Source: `packages/server/src/i18n/`
- `I18nManager.ts` — I18n manager
- `Translator.ts` — Translation engine
- `pluralization.ts` — Pluralization rules
Loaders: `JsonLoader.ts`, `MemoryLoader.ts`
App wiring: `createApp({ i18n: { supported: ['en', 'ja'] } })` loads
`lang/<locale>/*.json`, mounts locale detection (query → cookie →
Accept-Language), and shares the `_i18n` prop with Inertia pages.
Translate with `this.t()` / `this.tc()` / `this.locale` in controllers and
`useTranslation()` from `@guren/inertia-client` in pages. `guren codegen`
emits typed keys (`.guren/translations.gen.ts`); `guren check --i18n`
validates catalog parity and placeholders.
### Encryption
Source: `packages/server/src/encryption/`
- `Encrypter.ts` — Encrypt/decrypt values
- `Hash.ts` — Hashing utilities
- `Random.ts` — Secure random generation
### Health Checks
Source: `packages/server/src/health/`
- `HealthManager.ts` — Health check manager
- `HealthCheck.ts` — Base health check
Checks: `DatabaseCheck.ts`, `RedisCheck.ts`, `CacheCheck.ts`, `MemoryCheck.ts`, `StorageCheck.ts`, `CustomCheck.ts`
### Error Handling
Source: `packages/server/src/errors/`
- `ExceptionHandler.ts` — Global exception handler (supports duck-typed `statusCode` property)
- `HttpException.ts` — Base HTTP exception
- `debug-page.ts` — Debug error page
Exceptions: `NotFoundHttpException.ts`, `ValidationException.ts`, `AuthenticationException.ts`, `AuthorizationException.ts`, `MethodNotAllowedException.ts`
The ExceptionHandler automatically handles:
- `HttpException` subclasses → uses their status code
- Any error with a `statusCode` property (duck-typed) → uses that status code (e.g., `ModelNotFoundException` → 404)
- Other errors → 500 (message hidden unless debug mode)
Factory methods — throw directly from a controller method, no try/catch needed:
```typescript
HttpException.badRequest(msg?) // 400
HttpException.unauthorized(msg?) // 401
HttpException.forbidden(msg?) // 403
HttpException.notFound(msg?) // 404
HttpException.conflict(msg?) // 409
HttpException.unprocessable(msg?, errors?) // 422 — same errors shape as validateBody()
HttpException.internal(msg?) // 500
// also: methodNotAllowed / gone / tooManyRequests / notImplemented / badGateway / serviceUnavailable / gatewayTimeout
new ValidationException({ email: ['Already registered'] }) // 422
ValidationException.withMessages({ email: 'Already registered' }) // string | string[] values
new AuthenticationException(message?, guard?, redirectTo?) // 401
AuthenticationException.withRedirect(redirectTo, message?)
new AuthorizationException(message?, action?, resource?) // 403
AuthorizationException.deny(resource?) // e.g. AuthorizationException.deny('Comment')
AuthorizationException.forAction(action, resource?)
new NotFoundHttpException(message?) // 404
NotFoundHttpException.forModel('User', 123)
```
`Model.findOrFail()` throws `ModelNotFoundException` (exported from `@guren/core`) — it does *not* extend `HttpException`; the handler picks it up via its duck-typed `statusCode: 404`.
### Container & Service Providers
Source: `packages/server/src/container/`
- `Container.ts` — IoC container
- `ServiceProvider.ts` — Base service provider
Built-in providers: `packages/server/src/providers/`
### Console Commands
Source: `packages/server/src/console/`
- `Command.ts` — Base command class
- `ConsoleKernel.ts` — Console kernel
- `Input.ts` / `Output.ts` — IO handling
### API Resources
Source: `packages/server/src/http/resources/`
- `Resource.ts` — API resource transformer
- `ResourceCollection.ts` — Collection of resources
- `Paginator.ts` — Offset-based pagination
- `CursorPaginator.ts` — Cursor-based pagination
### Database (Factory & Seeder)
Source: `packages/server/src/database/`
- `Factory.ts` — Model factory for testing
- `Seeder.ts` — `BaseSeeder`/`Seeder`, deprecated in 2.9.0 (removed in 3.0.0)
- `SeederRunner.ts` — deprecated in 2.9.0 (removed in 3.0.0), wired to no command
Seeding itself does not live here. `db:seed` runs every seeder in `db/seeders/`,
and seeders are written with `defineSeeder` from `@guren/core`.
### Logging
Source: `packages/server/src/logging/`
- `LogManager.ts` — Log manager
- `Logger.ts` — Logger instance
Channels: `ConsoleChannel.ts`, `FileChannel.ts`, `DailyFileChannel.ts`
### Redis
Source: `packages/server/src/redis/`
- `client.ts` — Redis client
- Session, rate-limit, API token, email verification, password reset stores
## Reference Locations
| Subsystem | Source Path |
|-----------|------------|
| Controllers | `packages/server/src/mvc/Controller.ts` |
| Models | `packages/orm/src/Model.ts` |
| Routes | `packages/server/src/mvc/Route.ts` |
| Auth | `packages/server/src/auth/` |
| Testing | `packages/testing/src/` |
| Authorization | `packages/server/src/authorization/` |
| Events | `packages/server/src/events/` |
| Queue/Jobs | `packages/server/src/queue/` |
| Mail | `packages/server/src/mail/` |
| Cache | `packages/server/src/cache/` |
| Validation | `packages/server/src/http/validation/` |
| Broadcasting | `packages/server/src/broadcasting/` |
| Notifications | `packages/server/src/notifications/` |
| Storage | `packages/server/src/storage/` |
| Scheduling | `packages/server/src/scheduling/` |
| I18n | `packages/server/src/i18n/` |
| Encryption | `packages/server/src/encryption/` |
| Health Checks | `packages/server/src/health/` |
| Error Handling | `packages/server/src/errors/` |
| Container | `packages/server/src/container/` |
| Console | `packages/server/src/console/` |
| API Resources | `packages/server/src/http/resources/` |
| Database/Seeder | `packages/server/src/database/` |
| Logging | `packages/server/src/logging/` |
| Redis | `packages/server/src/redis/` |
| Example App | `examples/blog/` |
| API Example | `examples/api/` |
| Docs | `web/` |
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.