A comprehensive tutorial repository demonstrating how to integrate TanStack Start (React framework) with Effect HttpAPI (functional backend) in a modern Bun monorepo setup.
- Overview
- Features
- Architecture
- Prerequisites
- Quick Start
- Project Structure
- API Documentation
- Frontend Integration
- Development Workflow
- Type Safety & Error Handling
- Configuration
This repository demonstrates a production-ready integration between:
- Backend: Effect HttpAPI with functional programming patterns
- Frontend: TanStack Start with React 19 and TypeScript
- Monorepo: Bun workspace for optimal dependency management
- Type Safety: End-to-end type safety from database to UI
- Error Handling: Comprehensive error management with Effect's type system
- ๐ Type Safety: Bidirectional type safety between frontend and backend
- โก Performance: Bun's fast runtime and bundling
- ๐ก๏ธ Error Handling: Effect's robust error management system
- ๐ Real-time Ready: Built with live data synchronization in mind
- ๐ฆ Monorepo Benefits: Shared types and unified development experience
- โ Functional HTTP API with Effect
- โ Type-safe endpoints with automatic serialization
- โ Comprehensive error handling
- โ CORS configuration
- โ Swagger documentation
- โ Structured logging
- โ Service layer architecture
- โ React 19 with modern hooks
- โ TanStack Router for navigation
- โ TanStack Query for data fetching
- โ Custom Effect integration hooks
- โ Tailwind CSS + Radix UI components
- โ TypeScript with strict configuration
- โ Biome for formatting and linting
- โ Hot reload for both frontend and backend
- โ Shared TypeScript configuration
- โ Workspace dependencies
- โ Type generation and validation
- โ Development server setup
โโโโโโโโโโโโโโโโโโโ HTTP/JSON โโโโโโโโโโโโโโโโโโโ
โ TanStack โ โโโโโโโโโโโโโโโบ โ Effect โ
โ Start App โ โ HttpAPI โ
โ โ โ โ
โ โข React 19 โ โ โข Functional โ
โ โข TanStack โ โ โข Type-safe โ
โ Router โ โ โข Error โ
โ โข TanStack โ โ handling โ
โ Query โ โ โข Bun runtime โ
โ โข Custom hooks โ โ โข Swagger docs โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
- API Contract Definition: Effect HttpAPI endpoints with schemas
- Type Generation: Shared types between frontend and backend
- Data Fetching: Custom hooks wrapping TanStack Query + Effect
- Error Handling: Comprehensive error boundaries and user feedback
- State Management: TanStack Query for server state
Before getting started, ensure you have:
- Bun: Version 1.0+ (Install Bun)
- Git: For version control
bun --version# Clone the repository
git clone <repository-url>
cd quick-effect-tanstack
# Install dependencies for the entire monorepo
bun installTerminal 1 - Backend (Effect HttpAPI):
bun --filter=api run devTerminal 2 - Frontend (TanStack Start):
bun --filter=app run dev- Frontend: http://localhost:3000
- API Documentation: http://localhost:8080/docs (Swagger UI)
The application includes a fully functional Todo app demonstrating:
- Creating todos
- Listing all todos
- Toggling completion status
- Deleting individual or all todos
- Real-time updates via TanStack Query
quick-effect-tanstack/
โโโ ๐ฆ package.json # Root workspace configuration
โโโ ๐ง tsconfig.json # Shared TypeScript config
โโโ ๐ bun.lock # Dependency lock file
โโโ ๐ apps/
โโโ ๐ api/ # Effect HttpAPI Backend
โ โโโ ๐ฆ package.json
โ โโโ ๐ index.ts # Entry point
โ โโโ ๐ src/
โ โโโ ๐ฏ index.ts # Server setup
โ โโโ ๐ domains/
โ โโโ ๐ api.ts # API composition
โ โโโ ๐ index.ts
โ โโโ ๐ todos/
โ โโโ ๐ contract.ts # HTTP endpoints
โ โโโ ๐ dtos.ts # Data schemas
โ โโโ ๐ท๏ธ group.ts # API group
โ โโโ ๐ง service.ts # Business logic
โโโ ๐ app/ # TanStack Start Frontend
โโโ ๐ฆ package.json
โโโ โ๏ธ vite.config.ts
โโโ ๐จ biome.json
โโโ ๐ฆ components.json # Radix UI config
โโโ ๐ง tsconfig.json
โโโ ๐ public/ # Static assets
โโโ ๐ src/
โโโ ๐จ styles.css
โโโ ๐งญ router.tsx # Router setup
โโโ ๐ณ routeTree.gen.ts # Generated routes
โโโ ๐ components/
โ โโโ ๐งฉ Header.tsx
โ โโโ ๐ ui/ # Radix UI components
โโโ ๐ integrations/
โ โโโ ๐ tanstack-query/
โโโ ๐ lib/
โ โโโ ๐ data.ts # Effect + TanStack Query
โ โโโ ๐ ๏ธ utils.ts
โโโ ๐ routes/
โโโ ๐ __root.tsx
โโโ ๐ index.tsx # Todo app demo
| Method | Endpoint | Description |
|---|---|---|
GET |
/todos |
Get all todos |
POST |
/todos |
Create a new todo |
GET |
/todos/:id |
Get todo by ID |
PUT |
/todos/:id |
Update todo |
DELETE |
/todos/:id |
Delete todo |
DELETE |
/todos |
Delete all todos |
class Todo extends Schema.Class<Todo>("Todo")({
id: Schema.String,
title: Schema.String,
description: Schema.optional(Schema.String),
completed: Schema.Boolean,
createdAt: Schema.Date,
updatedAt: Schema.Date,
}) {}class CreateTodo extends Schema.Class<CreateTodo>("CreateTodo")({
title: Schema.String,
description: Schema.optional(Schema.String),
}) {}The API uses Effect's structured error handling:
HttpApiError.BadRequest- Invalid input dataHttpApiError.NotFound- Resource not foundHttpApiError.InternalServerError- Server errors
The frontend uses custom hooks that integrate Effect with TanStack Query:
const todos = useEffectQuery("todos", "getAllTodos", {});Benefits:
- Type-safe API calls
- Automatic error handling
- Caching and background updates
- Loading states
const createTodo = useEffectMutation("todos", "createTodo", {
onSuccess: () => {
todos.refetch();
},
});Features:
- Optimistic updates
- Error recovery
- Loading states
- Success callbacks
function TodoApp() {
const todos = useEffectQuery("todos", "getAllTodos", {});
const createTodo = useEffectMutation("todos", "createTodo", {
onSuccess: () => todos.refetch(),
});
const handleSubmit = (data: CreateTodo) => {
createTodo.mutate({ payload: data });
};
if (todos.isLoading) return <div>Loading...</div>;
if (todos.error) return <div>Error: {todos.error.message}</div>;
return (
<div>
{todos.data?.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
}-
Add New Endpoint:
// apps/api/src/domains/todos/contract.ts const newEndpoint = HttpApiEndpoint.get("newEndpoint", "/endpoint") .addSuccess(Schema.String) .addError(HttpApiError.BadRequest);
-
Implement Service:
// apps/api/src/domains/todos/service.ts const newEndpointImpl = (request: HttpRequest) => Effect.gen(function* () { // Implementation logic });
-
Update Group:
// apps/api/src/domains/todos/group.ts export const TodoGroup = HttpApiGroup.make("todos") .add(newEndpoint) .handleRaw(newEndpoint, newEndpointImpl);
-
Use the New Endpoint:
// Automatically available through the shared API type const data = useEffectQuery("todos", "newEndpoint", {});
-
Handle Loading States:
if (data.isLoading) return <Spinner />; if (data.error) return <ErrorMessage error={data.error} />; return <DataComponent data={data.data} />;
Both applications support hot reload:
- Backend: Automatic restart on file changes
- Frontend: Fast refresh with state preservation
-
Schema Definition (Backend):
class User extends Schema.Class<User>("User")({ id: Schema.String, email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), name: Schema.String, }) {}
-
API Contract (Backend):
const getUser = HttpApiEndpoint.get("getUser", "/users/:id") .setPath(Schema.Struct({ id: Schema.String })) .addSuccess(User) .addError(HttpApiError.NotFound);
-
Frontend Usage (Automatically typed):
const user = useEffectQuery("users", "getUser", { path: { id: "123" } }); // user.data is automatically typed as User | undefined
const getTodo = (id: string) =>
Effect.gen(function* () {
const todo = yield* findTodoById(id);
if (!todo) {
return yield* Effect.fail(new HttpApiError.NotFound());
}
return todo;
});const todos = useEffectQuery("todos", "getAllTodos", {
onError: (error) => {
console.error("Failed to fetch todos:", error);
// Show user-friendly error message
},
});SERVER_PORT=8080
SERVER_CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
LOG_LEVEL=infoVITE_API_URL=http://localhost:8080
VITE_APP_TITLE=Todo AppThe backend automatically configures CORS for development:
HttpApiBuilder.middlewareCors({
allowedOrigins: ["http://localhost:3000"],
credentials: true,
})The monorepo uses a shared TypeScript configuration with strict settings:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"moduleResolution": "bundler"
}
}