Fragno
Features

Config, Dependencies, and Services

Learn how to interface with the Fragment user.

The config is the basic contract between the Fragment author and the user. It is used to pass configuration to the Fragment, such as API keys and options. It can also be used to let the user react to events happening in the Fragment by providing callback functions.

Dependencies are objects that are available to route handlers. They are provided using the withDependencies method in the library definition. Dependencies are server-side only and are not included in the client bundle. They are private to the Fragment and cannot be used by the user directly.

Services are reusable business logic that can be exposed to Fragment users or shared between Fragments. Like dependencies, services are server-side only and not included in the client bundle. Fragments can both provide services (using providesService()) and require services (using usesService()), enabling composition and modularity.

Example Snippet

In the following snippet:

  1. The user is asked to provide an API key, as well as an optional callback function to be called when the text is summarized.
  2. A dependency is provided called aiWrapper. The withDependencies method has access to the config object. Using this to construct objects that require an API key is a common pattern.
  3. A service method called summarizeText is provided using providesService(). The service factory has access to both the config object and the dependencies. This can be used to make internal methods available to the user. Here, the callback function is called if it is provided.
src/index.ts
import { defineFragment } from "@fragno-dev/core";
import { MyOpenAIWrapper } from "./lib/my-openai-wrapper";

export interface MyFragmentConfig {
  openaiApiKey: string;
  model?: "gpt-5-mini" | "4o-mini" | "gpt-5-nano";
  onTextSummarized?: (input: string, output: string, tokensUsed: number) => void;
}

const myFragmentDefinition = defineFragment<MyFragmentConfig>("my-fragment")
  .withDependencies(({ config }) => {
    return {
      aiWrapper: new MyOpenAIWrapper({
        apiKey: config.openaiApiKey,
        model: config.model,
      }),
    };
  })
  .providesService(({ config, deps }) => {
    return {
      summarizeText: (text: string) => {
        const result = deps.aiWrapper.summarizeText(text);
        config.onTextSummarized?.(text, result.summary, result.tokensUsed);
        return result;
      },
    };
  });

Using defineRoutes to work with Dependencies and Services

The defineRoute function has a companion, defineRoutes (note the plural), which is a factory function that can be used to define multiple routes that have access to the dependencies and services.

The dependencies and services are scoped to the defineRoutes call. This means that we don't need global types for the dependencies and services, and can define them locally instead.

Another benefit of this approach is that we can define routes in a single TypeScript file (like the post-todos.ts file in the previous section). This makes the codebase easier to understand and maintain.

We define two types per defineRoutes call: one for the dependencies and one for the services. These types are passed to the call as generic parameters.

routes/post-todos.ts
import { defineRoute, defineRoutes } from "@fragno-dev/core";
import type { Todo } from "../index";
import type { MyOpenAIWrapper } from "../lib/my-openai-wrapper";

type AddTodoRouteDeps = {
  aiWrapper: MyOpenAIWrapper;
};

type AddTodoRouteServices = {
  summarizeText: (text: string) => { summary: string; tokensUsed: number };
};

export const addTodoRoute = defineRoutes<AddTodoRouteDeps, AddTodoRouteServices>().create(
  ({ config, deps, services }) => {
    const { aiWrapper } = deps;
    const { summarizeText } = services;

    return [
      /* Calls to `defineRoute` that use internal methods on `aiWrapper` or public methods such
         as `summarizeText` from the services. */
    ];
  },
);

The create callback should return one or more routes. The config, dependencies, and services are available in the callback.

The addTodoRoute object is a route factory that will be instantiated by a call to instantiateFragment(). At that point, the dependencies and services are passed to the route object.

The instantiateFragment function

The instantiateFragment function is used to create the Fragment instance. It provides a builder pattern that allows you to configure the Fragment with config, routes, services, and options. The final build() call creates the server-side object that contains the services field.

src/index.ts
import { instantiateFragment } from "@fragno-dev/core";
import type { FragnoPublicConfig } from "@fragno-dev/core";

import { getTodosRoute } from "./routes/get-todos";
import { addTodoRoute } from "./routes/post-todos";

// ... defineFragment call we've seen above ...

const routes = [getTodosRoute, addTodoRoute] as const;

export function createMyFragment(config: MyFragmentConfig, options: FragnoPublicConfig = {}) {
  return instantiateFragment(myFragmentDefinition)
    .withConfig(config)
    .withRoutes(routes)
    .withOptions(options)
    .build();
}

Providing and Requiring Services (Composition)

Fragments can work together by requiring services from other Fragments or the user. This enables modular architectures where Fragments depend on shared functionality.

Providing Named Services

Named services provide better organization and enable Fragments to declare what they offer:

import { defineFragment } from "@fragno-dev/core";

// A Fragment that provides email functionality
export const emailFragment = defineFragment<{ apiKey: string }>("email-fragment")
  .withDependencies(({ config }) => ({
    emailClient: new EmailClient(config.apiKey),
  }))
  .providesService("email", ({ deps, defineService }) =>
    defineService({
      sendEmail: async (to: string, subject: string, body: string) => {
        return deps.emailClient.send(to, subject, body);
      },
    }),
  );

Requiring Services

Other Fragments can require services using usesService():

interface IEmailService {
  sendEmail(to: string, subject: string, body: string): Promise<void>;
}

// A Fragment that requires email functionality
export const notificationFragment = defineFragment<{}>("notification-fragment")
  .usesService<"email", IEmailService>("email")
  .providesService(({ deps }) => ({
    sendWelcomeNotification: async (userId: string) => {
      // Use the required email service
      await deps.email.sendEmail(
        `user-${userId}@example.com`,
        "Welcome!",
        "Welcome to our service!",
      );
    },
  }));

// When instantiating, provide the required service
const emailInstance = instantiateFragment(emailFragment).withConfig({ apiKey: "key" }).build();

const notificationInstance = instantiateFragment(notificationFragment)
  .withConfig({})
  .withServices({
    email: emailInstance.services.email, // Pass the email service
  })
  .build();

Optional Services

Services can be marked as optional, making them undefined if not provided:

interface ILogger {
  log(message: string): void;
}

const fragment = defineFragment<{}>("app-fragment").usesService<"logger", ILogger>("logger", {
  optional: true,
});

// Can instantiate without providing the optional service
const instance = instantiateFragment(fragment).withConfig({}).build();
// instance.services.logger will be undefined

Database Integration

Fragments with database layers have access to an additional db object in both withDependencies() and providesService() contexts. The db object provides type-safe database operations:

Note that defineFragment is swapped out for defineFragmentWithDatabase when defining a fragment with a database layer.

import { defineFragmentWithDatabase } from "@fragno-dev/db/fragment";

const fragmentDef = defineFragmentWithDatabase("my-fragment")
  .withDatabase(mySchema)
  .withDependencies(({ config, db }) => {
    // DB available in dependencies
    return {
      commentRepo: {
        create: (data) => db.create("comment", data),
      },
    };
  })
  .providesService(({ config, deps, db }) => {
    // DB available in services
    return {
      createComment: async (data) => {
        const id = await deps.commentRepo.create(data);
        return { id: id.toJSON(), ...data };
      },
    };
  });

For more information, see the Database Integration section.