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,
      }),
    };
  })
  .providesBaseService(({ 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

Use defineRoutes(fragmentDefinition) to define routes that have access to your Fragment's config, dependencies, and services.

Because defineRoutes is typed from the fragment definition, you usually don't need to define separate Deps/Services types just to get good inference in your route handlers.

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.

routes/post-todos.ts
import { defineRoutes } from "@fragno-dev/core";
import { z } from "zod";
import { myFragmentDefinition } from "../definition";

export const routes = defineRoutes(myFragmentDefinition).create(
  ({ config, deps, services, defineRoute }) => [
    defineRoute({
      method: "POST",
      path: "/summarize",
      inputSchema: z.object({ text: z.string() }),
      handler: async ({ input }, { json }) => {
        const { text } = await input.valid();

        // deps + services are fully typed here based on myFragmentDefinition
        const result = deps.aiWrapper.summarizeText(text);
        config.onTextSummarized?.(text, result.summary, result.tokensUsed);

        return json(result);
      },
    }),
  ],
);

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 instantiate(). At that point, the dependencies and services are passed to the route object.

The instantiate function

The instantiate 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 { instantiate } 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 instantiate(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")
  .providesBaseService(({ 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 = instantiate(emailFragment).withConfig({ apiKey: "key" }).build();

const notificationInstance = instantiate(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 = instantiate(fragment).withConfig({}).build();
// instance.services.logger will be undefined

Service types

Fragments support three kinds of services:

  • Base services: Defined with providesBaseService(...). These methods are exposed directly on instance.services.
  • Named services: Defined with providesService("name", ...). These are grouped under instance.services.name (useful for composition and organization).
  • Private services: Defined with providesPrivateService("name", ...). These are not exposed to users, but can be used when constructing other services via the privateServices context.

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 when using withDatabase, the database context is available in dependencies and services.

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

const fragmentDef = defineFragment("my-fragment")
  .extend(withDatabase(mySchema))
  .withDependencies(({ config, db }) => {
    // DB available in dependencies
    return {
      commentRepo: {
        create: (data) => db.create("comment", data),
      },
    };
  })
  .providesBaseService(({ 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.