Handle your business logic
This guide starts a shop with no data yet: an operation, a page that calls it, a payment
provider you can swap, and the errors a payment can return. Everything happens in
fronds/shop/.
Handlers
A handler is a class in handlers/. Its public methods are what the rest of the app can
call.
// fronds/shop/handlers/CartHandler.ts
export default class CartHandler {
async getTotal(): Promise<number> {
return 4990;
}
}
Nothing else to write: no route, no registration. The file being in handlers/ is the
declaration.
The name tells Fougere whether the method reads or writes. A method reads when its name uses
only read verbs — get, list, find, count, search, has, is… — and every other
method writes: create, pay, publish, and also total or getOrCreate. A write is a
command: it answers over POST and is never cached. A read taken for a write still works,
which is why the rule leans that way; ASP.NET Web API settles a method with no known verb
the same way.
To make a read of a method whose name does not say so, state it for that one operation, naming it the way a call does — its address, then its name:
// fronds/shop/frond.config.ts
export default { operations: { 'cart.total': { kind: 'query' } } };
When a read verb of your domain starts many operations, teach it once instead.
A file in the frond's vocabulary/ exports it, and the process lists it beside its fronds:
// fronds/shop/vocabulary/verbs.ts
import { Verbs } from '@fougere/core';
export const total = Verbs.term('total', 'query');
// fronds.ts — written by the scan, or by hand
export const vocabulary = [total];
→ Handlers
The facade
A page does not import the handler. It imports its facade, a constant Fougere writes
for each handler in @fronds/facade:
<!-- app/pages/index.vue -->
<script setup lang="ts">
import { cart } from '@fronds/facade';
const { data: total } = await useQuery(cart, 'getTotal');
</script>
<template>
<p v-if="total !== null">Total: {{ total / 100 }} €</p>
</template>
cart carries the type of CartHandler: 'getTotal' is checked, and total is a
number | null — null until the query has answered.
Fougere's plugin for your host — @fougere/nuxt, @fougere/vite, withFougere() for
Next — writes this module from your handlers each time dev or build starts.
Services
A service is a class in services/ that a handler asks for in its constructor. Fougere
reads the type and hands an instance over.
A payment provider is the typical case. The handler asks for Payment, and never names
Stripe:
// fronds/shop/services/Payment.ts
export default abstract class Payment {
abstract charge(cents: number): Promise<{ reference: string }>;
}
// fronds/shop/services/StripePayment.ts
import Payment from './Payment.js';
export default class StripePayment extends Payment {
async charge(cents: number) {
// call Stripe here
return { reference: 'ch_123' };
}
}
// fronds/shop/handlers/CheckoutHandler.ts
import Payment from '../services/Payment.js';
export default class CheckoutHandler {
constructor(private payment: Payment) {}
async pay(cents: number): Promise<{ reference: string }> {
return this.payment.charge(cents);
}
}
StripePayment extends Payment is all it takes: Payment is now a port, and
StripePayment answers it. Add a second implementation — a FakePayment for
development — and Fougere refuses to start rather than pick one:
Fougere boot refused: 1 declaration(s) that do not hold:
[port-implemented-twice] Payment
FakePayment and StripePayment both extend Payment. Pick one in fougere.config.ts: ports: { Payment: 'FakePayment' }.
fronds/shop/services/FakePayment.ts
The choice goes in fougere.config.ts, and CheckoutHandler does not change:
export default defineFougere({
ports: { Payment: process.env.NODE_ENV === 'production' ? 'StripePayment' : 'FakePayment' },
});
→ Ports
Errors
An operation refuses with a FougereError and a code:
// fronds/shop/handlers/CheckoutHandler.ts
import { FougereError, ErrorCode } from '@fougere/core';
import Payment from '../services/Payment.js';
export default class CheckoutHandler {
constructor(private payment: Payment) {}
async pay(cents: number): Promise<{ reference: string }> {
if (cents <= 0) {
throw new FougereError({ code: ErrorCode.UNPROCESSABLE_ENTITY, message: 'Nothing to pay' });
}
return this.payment.charge(cents);
}
}
The facade adds where it was refused — error.address is checkout, error.operation
is pay — so the handler writes neither.
The page knows which codes pay can return — they are part of the facade's type:
const pay = useCommand(checkout, 'pay');
await pay.execute({ params: { cents: 0 } });
// pay.error.value?.code: ErrorCode.UNPROCESSABLE_ENTITY | …
The error reaches the page with the same code and message whether the frond runs in the app's process or on another server.
→ Errors
Next: Store your data.