Skip to content

Fastify Integration ​

WARNING

We created this page with the help of the GenAI tool.

We're currently double-checking it to ensure the information is 100% correct and free of hallucinations.

Emmett's Fastify integration provides a high-performance alternative to Express.js for building event-sourced web APIs.

Overview ​

The @event-driven-io/emmett-fastify package provides:

  • Fastify server setup - Sensible defaults with plugin system
  • Graceful shutdown - Proper cleanup of connections
  • Built-in plugins - ETag, compression, form body parsing
  • Testing utilities - Using Fastify's inject method

Installation ​

bash
npm install @event-driven-io/emmett-fastify

Peer Dependencies ​

bash
npm install @event-driven-io/emmett fastify

Quick Start ​

Basic Setup ​

typescript
import { startAPI, getFastifyApp } from '@event-driven-io/emmett-fastify';
import { getInMemoryEventStore } from '@event-driven-io/emmett';

const eventStore = getInMemoryEventStore();

const app = getFastifyApp({
  apis: [shoppingCartApi(eventStore)],
});

await startAPI(app, { port: 3000 });

Defining Routes ​

typescript
import type { FastifyInstance } from 'fastify';

export const shoppingCartApi =
  (eventStore: EventStore) => async (fastify: FastifyInstance) => {
    const handle = CommandHandler(eventStore, shoppingCartDecider);

    // GET - Read shopping cart
    fastify.get('/carts/:cartId', async (request, reply) => {
      const { cartId } = request.params as { cartId: string };

      const { state, currentStreamVersion } = await eventStore.aggregateStream(
        `shopping_cart-${cartId}`,
        { evolve, initialState },
      );

      if (currentStreamVersion === 0n) {
        return reply.status(404).send({ detail: 'Cart not found' });
      }

      return reply.header('ETag', `"${currentStreamVersion}"`).send(state);
    });

    // POST - Add product item
    fastify.post('/carts/:cartId/items', async (request, reply) => {
      const { cartId } = request.params as { cartId: string };
      const { productId, quantity } = request.body as {
        productId: string;
        quantity: number;
      };

      const result = await handle(cartId, {
        type: 'AddProductItem',
        data: { productId, quantity, price: await getPrice(productId) },
      });

      return reply
        .header('ETag', `"${result.nextExpectedStreamVersion}"`)
        .send({ success: true });
    });

    // POST - Confirm cart
    fastify.post('/carts/:cartId/confirm', async (request, reply) => {
      const { cartId } = request.params as { cartId: string };

      await handle(cartId, {
        type: 'ConfirmShoppingCart',
        data: { confirmedAt: new Date() },
      });

      return reply.send({ status: 'Confirmed' });
    });
  };

Complete Example ​

typescript
import { startAPI, getFastifyApp } from '@event-driven-io/emmett-fastify';
import { getPostgreSQLEventStore } from '@event-driven-io/emmett-postgresql';
import { CommandHandler } from '@event-driven-io/emmett';

// Define your decider
const shoppingCartDecider = {
  decide,
  evolve,
  initialState,
  mapToStreamId: (id: string) => `shopping_cart-${id}`,
};

// Create event store
const eventStore = getPostgreSQLEventStore(process.env.DATABASE_URL!);

// Define API
const shoppingCartApi =
  (eventStore: EventStore) => async (fastify: FastifyInstance) => {
    const handle = CommandHandler(shoppingCartDecider, eventStore);

    fastify.post<{
      Params: { cartId: string };
      Body: { productId: string; quantity: number };
    }>('/carts/:cartId/items', async (request, reply) => {
      const { cartId } = request.params;
      const { productId, quantity } = request.body;

      const price = await getPriceFromCatalog(productId);

      const result = await handle(cartId, {
        type: 'AddProductItem',
        data: { productId, quantity, price },
      });

      return reply
        .header('ETag', `"${result.nextExpectedStreamVersion}"`)
        .status(200)
        .send({ added: true });
    });
  };

// Start server
const app = getFastifyApp({
  apis: [shoppingCartApi(eventStore)],
});

await startAPI(app, { port: 3000 });

Default Plugins ​

Emmett's Fastify setup includes these plugins by default:

PluginPurpose
@fastify/etagAutomatic ETag generation
@fastify/compressResponse compression
@fastify/formbodyForm body parsing

Disabling Default Plugins ​

typescript
const app = getFastifyApp({
  apis: [myApi],
  disableDefaultPlugins: ['etag', 'compress'],
});

Custom Configuration ​

Server Options ​

serverOptions takes Fastify's full server options. Fastify's logger is off by default, as in Fastify itself. Turn it on with logger, or pass your own logger with loggerInstance.

typescript
const app = getFastifyApp({
  apis: [myApi],
  serverOptions: {
    logger: true,
    trustProxy: true,
    maxParamLength: 200,
  },
});

Custom Plugins ​

typescript
import cors from '@fastify/cors';
import helmet from '@fastify/helmet';

const app = getFastifyApp({
  apis: [myApi],
  plugins: [
    [cors, { origin: true }],
    [helmet, {}],
  ],
});

Before/After Hooks ​

typescript
const app = getFastifyApp({
  apis: [myApi],
  beforeRoutes: async (fastify) => {
    // Register plugins, decorators, etc.
    await fastify.register(authPlugin);
  },
  afterRoutes: async (fastify) => {
    // Add error handlers, not-found handlers, etc.
    fastify.setNotFoundHandler((request, reply) => {
      reply.status(404).send({ error: 'Not found' });
    });
  },
});

Graceful Shutdown ​

Emmett handles graceful shutdown automatically:

typescript
await startAPI(app, {
  port: 3000,
  onClose: async () => {
    // Clean up resources
    await eventStore.close();
    await database.disconnect();
  },
});

// Handles SIGINT, SIGTERM gracefully

Testing ​

Using Fastify Inject ​

typescript
describe('Shopping Cart API', () => {
  let app: FastifyInstance;

  beforeAll(async () => {
    const eventStore = getInMemoryEventStore();

    app = getFastifyApp({
      apis: [shoppingCartApi(eventStore)],
    });

    await app.ready();
  });

  afterAll(async () => {
    await app.close();
  });

  it('adds product to cart', async () => {
    const response = await app.inject({
      method: 'POST',
      url: '/carts/123/items',
      payload: {
        productId: 'shoes-1',
        quantity: 2,
      },
    });

    expect(response.statusCode).toBe(200);
    expect(JSON.parse(response.body)).toEqual({ added: true });
  });

  it('returns 404 for missing cart', async () => {
    const response = await app.inject({
      method: 'GET',
      url: '/carts/nonexistent',
    });

    expect(response.statusCode).toBe(404);
  });
});

With Different Event Stores ​

typescript
describe('Shopping Cart API (PostgreSQL)', () => {
  let postgres: StartedPostgreSqlContainer;
  let app: FastifyInstance;

  beforeAll(async () => {
    postgres = await new PostgreSqlContainer().start();
    const eventStore = getPostgreSQLEventStore(postgres.getConnectionUri());

    app = getFastifyApp({
      apis: [shoppingCartApi(eventStore)],
    });

    await app.ready();
  });

  afterAll(async () => {
    await app.close();
    await postgres.stop();
  });

  // Tests...
});

Type Safety ​

Fastify provides excellent TypeScript support:

typescript
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

// Define route schemas
interface AddItemParams {
  cartId: string;
}

interface AddItemBody {
  productId: string;
  quantity: number;
}

interface AddItemResponse {
  added: boolean;
  newTotal: number;
}

fastify.post<{
  Params: AddItemParams;
  Body: AddItemBody;
  Reply: AddItemResponse;
}>(
  '/carts/:cartId/items',
  {
    schema: {
      params: {
        type: 'object',
        properties: {
          cartId: { type: 'string' },
        },
        required: ['cartId'],
      },
      body: {
        type: 'object',
        properties: {
          productId: { type: 'string' },
          quantity: { type: 'integer', minimum: 1 },
        },
        required: ['productId', 'quantity'],
      },
    },
  },
  async (request, reply) => {
    // request.params.cartId is typed as string
    // request.body.productId is typed as string
    // request.body.quantity is typed as number
  },
);

Error Handling ​

typescript
fastify.setErrorHandler((error, request, reply) => {
  if (error instanceof ValidationError) {
    return reply.status(400).send({
      type: 'about:blank',
      title: 'Bad Request',
      status: 400,
      detail: error.message,
    });
  }

  if (error instanceof IllegalStateError) {
    return reply.status(403).send({
      type: 'about:blank',
      title: 'Forbidden',
      status: 403,
      detail: error.message,
    });
  }

  // Log unexpected errors
  request.log.error(error);

  return reply.status(500).send({
    type: 'about:blank',
    title: 'Internal Server Error',
    status: 500,
    detail: 'An unexpected error occurred',
  });
});

Full Package Documentation ​

For complete API reference and advanced usage, see the package README.

See Also ​