fronds/blog/entities/Post.ts
1import { entity, oneOf, primary, readOnly, text } from '@fougere/schema';
2
3export default class Post extends entity({
4 id: primary(),
5 title: text({ min: 1, max: 160 }),
6 status: readOnly(oneOf('draft', 'published', { default: 'draft' })),
7}) {}
fronds/blog/entities/PostPublished.ts
1import Post from './Post.js';
2
3export default class PostPublished extends Post.pick('id', 'title') {}
fronds/blog/handlers/PostHandler.ts
1import { ErrorCode, FougereError, Repository, type Emit } from '@fougere/core';
2import Post from '../entities/Post.js';
3import type PostPublished from '../entities/PostPublished.js';
4
5export default class PostHandler extends Repository<Post> {
6 constructor(private published: Emit<PostPublished>) {
7 super();
8 }
9
10 async publish(id: Post['id']): Promise<Post> {
11 const post = await this.findById(id);
12 if (!post) throw new FougereError({ code: ErrorCode.NOT_FOUND, message: 'No such post' });
13 if (post.status === 'published') throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already published' });
14
15 await this.published({ id: post.id, title: post.title });
16
17 return this.update(id, { status: 'published' });
18 }
19}
fronds/mail/handlers/MailHandler.ts
1import type { Fact } from '@fougere/core';
2import type PostPublished from '@fronds/blog/entities/PostPublished.js';
3import Mailer from '../services/Mailer.js';
4
5export default class MailHandler {
6 constructor(private mailer: Mailer) {}
7
8 async onPublished(post: Fact<PostPublished>): Promise<void> {
9 await this.mailer.send(`Published: ${post.title}`);
10 }
11}
fronds/mail/services/AuditMailer.ts
1import type { Logger } from '@fougere/core';
2import type Mailer from './Mailer.js';
3
4export default class AuditMailer implements Partial<Mailer> {
5 constructor(private inner: Mailer, private log: Logger) {}
6
7 async send(subject: string): Promise<void> {
8 this.log.info('sending', subject);
9
10 return this.inner.send(subject);
11 }
12}
fronds/mail/services/ConsoleMailer.ts
1import Mailer from './Mailer.js';
2
3export default class ConsoleMailer extends Mailer {
4 async send(subject: string): Promise<void> {
5 console.log(`mail: ${subject}`);
6 }
7}
fronds/mail/services/Mailer.ts
1export default abstract class Mailer {
2 abstract send(subject: string): Promise<void>;
3}
apps/nuxt/app/pages/posts.vue
1<script setup lang="ts">
2import { post } from '@fronds/facade';
3import Post from '@fronds/blog/entities/Post.js';
4
5const { items } = await useQuery(post, 'list');
6const { fields, values, errors, submit } = useFormFor(Post);
7const publish = useCommand(post, 'publish');
8</script>
9
10<template>
11 <form novalidate @submit.prevent="submit">
12 <template v-for="field in fields" :key="field.name">
13 <input v-model="values[field.name]" v-bind="field.attrs" :placeholder="field.label" />
14 <small v-if="errors[field.name]">{{ errors[field.name] }}</small>
15 </template>
16 <button>Write a draft</button>
17 </form>
18
19 <ul>
20 <li v-for="row in items" :key="row.id">
21 {{ row.title }} — {{ row.status }}
22 <button v-if="row.status === 'draft'" @click="publish.execute({ params: { id: row.id } })">Publish</button>
23 </li>
24 </ul>
25 <p v-if="publish.error.value">{{ publish.error.value.message }}</p>
26</template>
fougere.config.ts
1import '@fougere/adapter-rest';
2import '@fougere/adapter-sql';
3import { defineFougere } from '@fougere/core';
4
5export default defineFougere({
6 protocols: { rest: {} },
7 sources: { main: { type: 'sqlite', path: '.data/blog.db' } },
+ fronds: {
+ '@fougere/log': {},
+ mail: { remote: 'http://127.0.0.1:4100' },a b c d
+ },
12});
apps/nuxt/app/app.vue
1<template>
2 <NuxtPage />
3</template>
apps/nuxt/nuxt.config.ts
1// https://nuxt.com/docs/api/configuration/nuxt-config
2export default defineNuxtConfig({
3 compatibilityDate: '2025-07-15',
4 devtools: { enabled: true },
5 modules: ['@fougere/nuxt'],
6})
apps/nuxt/package.json
1{
2 "name": "nuxt",
3 "type": "module",
4 "private": true,
5 "scripts": {
6 "build": "nuxt build",
7 "dev": "nuxt dev",
8 "generate": "nuxt generate",
9 "preview": "nuxt preview",
10 "postinstall": "nuxt prepare"
11 },
12 "dependencies": {
13 "@fougere/container": "latest",
14 "@fougere/core": "latest",
15 "@fougere/defaults": "latest",
16 "@fougere/log": "latest",
17 "@fougere/nuxt": "latest",
18 "@fougere/schema": "latest",
19 "nuxt": "^4.5.2",
20 "vue": "^3.5.43",
21 "vue-router": "^5.3.1"
22 },
23 "devDependencies": {
24 "@fougere/compiler": "latest"
25 }
26}
package.json
1{
2 "name": "my-blog",
3 "private": true,
4 "type": "module",
5 "workspaces": [
6 "apps/*"
7 ],
8 "scripts": {
9 "typecheck": "tsc",
10 "test": "vitest run --config tests/vitest.config.ts"
11 },
12 "dependencies": {
13 "@fougere/adapter-rest": "latest",
14 "@fougere/adapter-sql": "latest",
15 "@fougere/core": "latest",
16 "@fougere/log": "latest",
17 "@fougere/schema": "latest",
18 "better-sqlite3": "^13.0.3"
19 },
20 "devDependencies": {
21 "@fougere/cli": "latest",
22 "@fougere/testing": "latest",
23 "@types/node": "^22.19.15",
24 "typescript": "^7.0.2",
25 "vitest": "^4.1.0"
26 }
27}