Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/applications/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,31 @@ report, err := c.service.Generate(ctx.Context(), input)

Use `web.Context` for HTTP-specific behavior such as params, binding, response helpers, request metadata, and response writing.

## Cacheable File Responses

When a controller returns an image or another stable file, choose an explicit browser cache policy and support revalidation when the content has a stable version. Keep authorization and storage lookup in the service; the controller translates the returned representation into HTTP headers and status codes.

<!-- go-example: illustrative-fragment -->
```go
func (c *Controller) Image(r web.Context) error {
image, err := c.service.Image(r.Context(), r.Param("id"))
if err != nil {
return err
}

etag := `"` + image.Digest + `"`
r.SetHeader("Cache-Control", "private, max-age=0, must-revalidate")
r.SetHeader("ETag", etag)
if r.Request().Header.Get("If-None-Match") == etag {
return r.NoContent(http.StatusNotModified)
}

return r.Blob(http.StatusOK, image.ContentType, image.Body)
}
```

The cache policy depends on the resource. Public immutable assets can use a long lifetime and content-addressed URL. Private or replaceable media usually needs a validator and an authorization decision on each revalidation. Test both the initial `200` response and the matching conditional request that returns `304 Not Modified` without a response body.

## Next Steps

- [JSON API Route](/scenarios/json-api-route) follows a complete controller, service, test, build, route-list, and request workflow.
Expand Down
17 changes: 17 additions & 0 deletions docs/applications/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,23 @@ func (s *Service) Create(ctx context.Context, input CreateReportInput) (Report,

This keeps service APIs independent from HTTP request structs, CLI flag structs, and queue payload structs.

When several returned values describe one application outcome, give that outcome a name rather than growing a tuple:

<!-- go-example: illustrative-fragment -->
```go
type CreateReportResult struct {
Report Report
Queued bool
DeliveryTime time.Time
}

func (s *Service) Create(ctx context.Context, input CreateReportInput) (CreateReportResult, error) {
// ...
}
```

Keep conventional and naturally small returns such as `(Report, error)`. The point is to make a cohesive result easier to understand and extend, not to replace every pair of values with a struct.

## Runtime Boundaries

Multiple entry points may call the same service:
Expand Down
24 changes: 24 additions & 0 deletions docs/data/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@ func (s *Service) Find(ctx context.Context, id string) (User, error) {

Keep service inputs independent from database model structs unless that type is intentionally the application model.

## Relationships

Use `forj make:model <table> --package <package>` when an existing table needs the conventional schema-derived model and repository scaffold. Inspect the real table, foreign keys, and nearby package ownership first. The generator derives columns from the selected table and reads supported relationship declarations from `.db-relationships.yaml`.

For example, this declaration gives generated users their related posts while keeping the key mapping explicit and schema-validated:

```yaml
users:
- "1-many id->posts:user_id"
```

Generate the referenced model in the same package before the model that exposes it:

```bash
forj make:model posts --package content
forj make:model users --package content
```

The generated user model receives the relationship field and reports its eager-loading path through `Relationships()`.

The config owns the generated relationship fields. Repositories still own persistence-specific joins, preloads, and mapping across related rows. Return a domain or application result that expresses what the caller needs rather than making controllers, commands, jobs, or frontend code navigate database relationships directly.

For example, a billing repository can return `InvoiceDetails` containing an invoice and its line items. Whether that query uses a join, preload, or separate bounded reads stays behind the repository method and can change without rewriting its callers.

## Named Connections

Use named connections when a feature has a real persistence boundary:
Expand Down
2 changes: 2 additions & 0 deletions docs/data/storage-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ This page covers your App's generated disks and their configuration. The [storag

Use storage when a workflow produces or consumes files, blobs, exports, uploads, or remote objects. Start with local or memory storage for development and tests. Choose object storage or a remote filesystem when more than one host or process needs the same files.

When a feature introduces a durable category such as avatars, invoice attachments, or generated reports, consider giving it a named storage disk. A distinct name is valuable when the category has its own access, retention, visibility, or deployment policy. Reuse an existing disk when those policies are genuinely shared; do not create names merely to mirror directories.

Keep relational metadata, ownership, authorization state, and transactional updates in the database.

## Access Storage from Application Code
Expand Down
10 changes: 10 additions & 0 deletions docs/frontend/react-starter-kit.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ npm install
npm run dev
```

## Load Data Without Flicker

Keep feature-local server data in the route or component that owns it, or in a colocated hook. Global context is appropriate for genuinely application-wide client state; it should not become the default home for every request merely to share loading machinery.

Delay transient skeletons or spinners briefly. If a fast request resolves before that threshold, commit the content directly without flashing a pending state. During refresh or revalidation, retain the current usable content until replacement data is ready instead of clearing the view.

Still model explicit loading, error, empty, and ready states. A slower request should reveal a stable pending layout with the same general dimensions as the result, while a completed request should never be artificially delayed to satisfy an animation.

Route-critical data can load before navigation commits. Predictable modal and detail data can be prefetched so opening the interface does not immediately tear down and replace its contents.

## Build for Deployment

The default deployment remains one App binary. React compiles into `frontend/dist`, then `forj build` embeds those assets alongside the Go application.
Expand Down
8 changes: 8 additions & 0 deletions docs/frontend/templ-htmx-starter-kit.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ The starter uses `hx-boost` on its navigation. A click still performs a normal `

This means the route, controller, and page remain useful when JavaScript is unavailable. htmx enhances the request rather than defining a separate browser-side routing model.

### Keep Pending UI Stable

Resolve data required by the initial page contract in the controller or service before rendering the page. Do not send a page that visually presents itself as complete and then immediately replaces its primary contents when required data arrives.

For htmx refreshes, keep the existing target visible while the request is pending. Delay transient progress indicators so a fast response does not flash a loader, and use a stable target whose dimensions do not collapse before the swap. If the request fails, replace or annotate the target with an intentional error state rather than leaving it blank.

Progressive regions are still useful when they are genuinely independent. The distinction is ownership: required page data should be ready for the page render, while an independently useful region may load separately without making the rest of the page appear unfinished.

### Add a Partial Update

The starter pages return complete documents. When one interaction benefits from replacing a smaller region, render a smaller templ component from a dedicated handler.
Expand Down
10 changes: 10 additions & 0 deletions docs/frontend/vue-starter-kit.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ forj dev

for the default local App lifecycle.

## Load Data Without Flicker

Keep feature-local server data in the view that owns it or in a colocated composable. A global store is useful for genuinely application-wide client state; it should not become the default home for every request merely to centralize loading behavior.

Fast requests need a different pending treatment from slow ones. Delay a transient skeleton or spinner briefly. If the request resolves before that threshold, render the result directly and never show the pending state. Once useful content is visible, keep it visible during refresh or revalidation instead of replacing it with a skeleton.

The view should still define intentional loading, error, empty, and ready states. Match pending layout dimensions to the completed view when a slower request does make loading visible, and do not delay successfully loaded content simply to keep an animation on screen.

For data that must exist before a route makes sense, prefer loading it before committing that route. Prefetch predictable modal or detail data when practical so opening the interface does not immediately replace its contents.

## Build for Deployment

The default deployment remains one App binary. Vue compiles into `frontend/dist`, then `forj build` embeds those assets alongside the Go application.
Expand Down
12 changes: 11 additions & 1 deletion docs/reference/make-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,16 @@ forj make:model invoices --package billing

The positional argument is the exact name of an existing table. The generator inspects `invoices` through the default database connection, so that connection must be available. It does not create or migrate the table, and `make:model` does not select a named connection. The generated Go type and filename are singularized from the inspected table name; when an exact table is missing, the command may suggest an existing singular or plural variant. Models use `--package` rather than `-d` because their placement follows database table ownership.

By default, `make:model` also reads `.db-relationships.yaml`. Use `--config <path>` when the Project keeps that relationship contract elsewhere. Relationship declarations are explicit so the generator can validate every referenced local, remote, and join key against the active schema before changing source.

```yaml
users:
- "1-many id->posts:user_id"
- "many-many id->roles:id via user_roles:user_id:role_id"
```

Supported declarations cover one-to-many, many-to-many, and polymorphic relationships. Generate every referenced model in the same package when the resulting fields refer to one another. Keep application-specific query selection, preload policy, and result mapping in repository methods rather than making transports navigate persistence relationships.

```bash
forj make:model invoices --package billing --remove
```
Expand Down Expand Up @@ -1164,7 +1174,7 @@ Generated files are starting points. Your App still owns:
- schedule intervals and handler behavior
- event payloads and subscribers
- migration SQL
- model relationships and repository options
- repository query, preload, and persistence options beyond the generated relationship contract

Keep dependencies explicit. If a generated controller, command, or job needs an application service, add that service constructor to the right provider set and let Wire pass it in.

Expand Down
18 changes: 11 additions & 7 deletions docs/scenarios/cached-user-profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,15 @@ CACHE_PROFILES_DEFAULT_TTL_SECONDS=300
CACHE_PROFILES_PREFIX=profiles
```

## Step 2: Refresh Cache Resources

Run the build pipeline after changing named resource configuration.

```bash
forj build
```

## Step 2: Add the Repository
## Step 3: Add the Repository

Create `internal/users/repository.go`.

Expand Down Expand Up @@ -190,7 +194,7 @@ func profileCacheKey(id string) string {
}
```

## Step 3: Use the Repository in the Service
## Step 4: Use the Repository in the Service

Replace `internal/users/service.go`.

Expand Down Expand Up @@ -237,7 +241,7 @@ func (s *Service) Find(ctx context.Context, id string) (User, error) {
}
```

## Step 4: Wire the Repository and Cache
## Step 5: Wire the Repository and Cache

Open `app/wire/inject_services_app.go`.

Expand All @@ -252,7 +256,7 @@ import (
"your/module/internal/caches"
```

## Step 5: Add Repository Providers
## Step 6: Add Repository Providers

Add the source repository, cached repository provider, and named cache provider.

Expand All @@ -265,7 +269,7 @@ provideUserRepository,
users.NewService,
```

## Step 6: Add Provider Functions
## Step 7: Add Provider Functions

`provideUserProfileCache` selects the named resource. `provideUserRepository` keeps the service wired to the repository interface.

Expand All @@ -283,7 +287,7 @@ func provideUserProfileCache(manager *caches.Manager) *cache.Cache {
}
```

## Step 7: Add Repository Tests
## Step 8: Add Repository Tests

Create `internal/users/repository_test.go`.

Expand Down Expand Up @@ -339,7 +343,7 @@ func TestCachedUserRepositoryFindsAndCachesUser(t *testing.T) {
}
```

## Step 8: Update the Service Test
## Step 9: Update the Service Test

Keep the service test focused on service behavior.

Expand Down
30 changes: 19 additions & 11 deletions docs/scenarios/file-upload-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ Do not edit generated storage files by hand.

Add a named `uploads` disk to `.env`, then run the build pipeline so your App exposes `app.Storage().Uploads()`.

Update `.env` so it includes:

```dotenv
STORAGE_SUPPORTED_DRIVERS=local,memory
```

## Step 2: Configure the Uploads Disk

Keep the named disk's driver and path in application configuration.

Append to `.env`:

```dotenv
Expand All @@ -88,25 +98,23 @@ STORAGE_UPLOADS_ROOT=storage/app/uploads
STORAGE_UPLOADS_PREFIX=
```

Update `.env` so it includes:
## Step 3: Refresh Storage Resources

```dotenv
STORAGE_SUPPORTED_DRIVERS=local,memory
```
Run the build pipeline after changing named resource configuration.

```bash
forj build
```

## Step 2: Scaffold the Controller
## Step 4: Scaffold the Controller

Start with the real make command. It creates the uploads controller, wires the constructor, and registers its routes.

```bash
forj make:controller uploads
```

## Step 3: Add the Service
## Step 5: Add the Service

Create `internal/uploads/service.go`.

Expand Down Expand Up @@ -221,7 +229,7 @@ func safeFilename(name string) string {
}
```

## Step 4: Replace the Starter Controller
## Step 6: Replace the Starter Controller

Replace `internal/uploads/controller.go`.

Expand Down Expand Up @@ -294,7 +302,7 @@ func (c *Controller) Store(ctx web.Context) error {
}
```

## Step 5: Add Upload Imports
## Step 7: Add Upload Imports

Add imports for the generated storage manager and uploads package.

Expand All @@ -306,7 +314,7 @@ Update `app/wire/inject_services_app.go` so it includes:
"your/module/internal/uploads"
```

## Step 6: Add Upload Providers
## Step 8: Add Upload Providers

Add the upload service provider, which selects its named disk at the composition root.

Expand All @@ -317,7 +325,7 @@ provideUploadsService,
provideUserProfileCache,
```

## Step 7: Add the Upload Service Provider
## Step 9: Add the Upload Service Provider

`provideUploadsService` keeps named disk selection out of application behavior without exporting an ambiguous `storage.Storage` to Wire.

Expand All @@ -333,7 +341,7 @@ func provideUploadsService(manager *storages.Manager) *uploads.Service {
func provideUserRepository(source *users.MemoryUserRepository, profileCache *cache.Cache) users.UserRepository {
```

## Step 8: Add a Service Test
## Step 10: Add a Service Test

Create `internal/uploads/service_test.go`.

Expand Down
Loading