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:
- The user is asked to provide an API key, as well as an optional callback function to be called when the text is summarized.
- A dependency is provided called
aiWrapper. ThewithDependenciesmethod has access to the config object. Using this to construct objects that require an API key is a common pattern. - A service method called
summarizeTextis provided usingprovidesService(). 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.
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.
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.
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 undefinedDatabase 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.