Getting Started
Learn how to build your own Fragno fragments
There are several concepts you should be familiar with before publishing a Fragment:
- Defining Routes
- Dependencies and Services
- Client-side State Management
- Code-Splitting (Client and Server bundles)
But before getting into those details, we'll start with the basics. In this walkthrough, we'll create two simple server routes for getting and creating todos, and then create a client-side Fragment that provides TanStack Query-style hooks for the two routes.
Installation
Before getting started, add the core package to your project as a regular dependency. You'll also need a Standard Schema compatible library for schema validation, such as Zod.
npm install @fragno-dev/core
Creating Your First Fragment
Let's build a simple todos fragment that demonstrates the core concepts.
Step 1: Define Your Fragment
import { defineFragment, defineRoute, createFragment } from "@fragno-dev/core";
import { createClientBuilder } from "@fragno-dev/core/client";
import { z } from "zod";
// Any configuration the users of your Fragment will need to provide
export interface TodosConfig {
openaiApiKey: string;
onTodoCreated?: (todo: Todo & { textSummary?: string }) => void;
}
export type Todo = z.infer<typeof TodoSchema>;
const TodoSchema = z.object({
id: z.string(),
text: z.string(),
done: z.boolean(),
createdAt: z.string(),
});
const todosDefinition = defineFragment<TodosConfig>("todos-fragment");
Step 2: Add Dependencies and Services (Optional)
Dependencies are private to the library/fragment and are not included in the client bundle. They can
be used in the server-side route handlers. Dependencies can be defined using the withDependencies
method, which has access to the config object.
import { defineFragment } from "@fragno-dev/core";
import { MyOpenAIWrapper } from "./lib/my-openai-wrapper";
const todosDefinition = defineFragment<TodosConfig>("todos-fragment").withDependencies(
(config: TodosConfig) => {
return {
aiWrapper: new MyOpenAIWrapper({
apiKey: config.openaiApiKey,
}),
};
},
);
We'll skip over services for now, but you can read more about them in the Dependencies and Services page.
Step 3: Define Routes
A basic route can be defined using the defineRoute
function. Options are: method
, path
,
inputSchema
, outputSchema
, handler
, errorCodes
, and queryParameters
. See our
Route Definition page for additional details.
The first argument passed to the handler is the input context object, which has an input
field
with a valid
method that can be used to validate the input. It also contains other fields related
to the request.
The second argument passed to the handler is the output context object, which contains several
method fields: json
, jsonStream
, empty
, and error
. These methods can be used to return a
response.
import { defineRoute } from "@fragno-dev/core";
export const getTodosRoute = defineRoute({
method: "GET",
path: "/todos",
outputSchema: z.array(TodoSchema),
handler: async ({ query }, { json }) => {
return json([
{
id: "1",
text: "Learn Fragno",
done: query.get("done") === "true",
createdAt: new Date().toISOString(),
},
]);
},
});
The defineRoute
function does not have access to the dependencies or services. These are
available in the route factory context object that can be accessed by using defineRoutes
(note the
plural).
import { defineRoute, defineRoutes } from "@fragno-dev/core";
import type { Todo } from "../index";
import type { MyOpenAIWrapper } from "../lib/my-openai-wrapper";
// Dependencies and services can be defined on a per-route level, since they might be a subset of
// all dependencies/services.
type AddTodoRouteDeps = {
aiWrapper: MyOpenAIWrapper;
};
export const addTodoRoute = defineRoutes<AddTodoRouteDeps, {}>().create(({ config, deps }) => {
const { aiWrapper } = deps;
return [
defineRoute({
method: "POST",
path: "/todos",
inputSchema: z.object({ text: z.string() }),
outputSchema: TodoSchema,
queryParameters: ["summarize"],
errorCodes: ["SUMMARY_ERROR"],
handler: async ({ input, query }, { json, error }) => {
const { text } = await input.valid();
const todo = {
id: crypto.randomUUID(),
text,
done: false,
createdAt: new Date().toISOString(),
} satisfies Todo;
try {
const textSummary = query.get("summarize")
? await aiWrapper.summarizeText(text)
: undefined;
} catch {
return error({ message: "Summary error", code: "SUMMARY_ERROR" }, 503);
}
// Call the user's callback if provided
config?.onTodoCreated?.({
...todo,
textSummary,
});
return json(todo);
},
}),
];
});
Step 4: Create the Server-Side Fragment
To finalize the server-side Fragment, we need to export a function that creates the library
instance. We pass the library definition, the config, and the routes to the createFragment
function.
import { getTodosRoute } from "./routes/get-todos";
import { addTodoRoute } from "./routes/post-todos";
import { createFragment } from "@fragno-dev/core";
import type { FragnoPublicConfig } from "@fragno-dev/core";
export function createTodos(config: TodosConfig, fragnoConfig: FragnoPublicConfig = {}) {
return createFragment(todosDefinition, config, [getTodosRoute, addTodoRoute], fragnoConfig);
}
Step 5: Create the Client-Side Fragment
The last step is to create the client-side Fragment. This object defines how your users will use the Fragment on the client-side. These are React-style hooks (or Vue composables, etc). In this example, we simply create hooks for the two routes we defined.
import { getTodosRoute } from "./routes/get-todos";
import { addTodoRoute } from "./routes/post-todos";
import { createClientBuilder } from "@fragno-dev/core/client";
import type { FragnoPublicClientConfig } from "@fragno-dev/core";
export function createTodosClient(fragnoConfig: FragnoPublicClientConfig = {}) {
const cb = createClientBuilder(todosDefinition, fragnoConfig, [getTodosRoute, addTodoRoute]);
return {
useTodos: cb.createHook("/todos"),
useAddTodo: cb.createMutator("POST", "/todos"),
};
}
Next Steps
We went over building a basic Fragment. In the next section, we'll cover the advanced features of Fragno. It's also important to review the Code Splitting page, which goes over bundling and publishing your Fragment.