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.

→ Queries & commands

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.

Built with Fougere and Nuxt, hosted on GitHub Pages.