Set up a full-stack FuryStack monorepo step by step — shared models, a backend service, and a Shades frontend — learning every piece along the way.
1. Workspace setup
Create a new directory and initialize a Yarn 4 monorepo with three workspaces:
common, service, and frontend.
mkdir my-furystack-app && cd my-furystack-app
yarn init -p
mkdir common service frontend
Edit the root package.json to configure workspaces, ESM, and a few convenience
scripts:
{
"name": "my-furystack-app",
"private": true,
"type": "module",
"workspaces": {
"packages": ["common", "service", "frontend"]
},
"scripts": {
"build": "tsc -b common service frontend && yarn workspace frontend build",
"start:service": "yarn workspace service start",
"start:frontend": "yarn workspace frontend start"
},
"devDependencies": {
"typescript": "^5.9.0"
}
}
Create a root tsconfig.json with project references so
tsc -b builds everything in the right order:
{
"compilerOptions": {
"composite": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "es2022",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"esModuleInterop": true,
"skipLibCheck": true
},
"references": [
{ "path": "common" },
{ "path": "service" },
{ "path": "frontend" }
],
"files": []
} 2. Common — shared models & API
The common workspace holds models and the REST API type definition. Both service and frontend depend on it, so API changes are instantly visible on both sides.
common/package.json
{
"name": "common",
"private": true,
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"dependencies": {
"@furystack/rest": "^8.1.0"
}
} common/tsconfig.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
} common/src/index.ts
Define a TodoItem model and a REST API contract:
import type { RestApi } from '@furystack/rest'
export interface TodoItem {
id: string
title: string
completed: boolean
}
export interface MyApi extends RestApi {
GET: {
'/todos': { result: TodoItem[] }
'/todos/:id': { url: { id: string }; result: TodoItem }
}
POST: {
'/todos': {
body: { title: string }
result: TodoItem
}
}
} 3. Service — the backend
The service is a Node.js process that uses FuryStack to set up an HTTP API driven by the
shared MyApi type.
service/package.json
{
"name": "service",
"private": true,
"type": "module",
"scripts": {
"start": "yarn node ./dist/index.js"
},
"dependencies": {
"@furystack/core": "^15.2.0",
"@furystack/inject": "^12.0.0",
"@furystack/logging": "^8.1.0",
"@furystack/repository": "^10.1.0",
"@furystack/rest-service": "^12.3.0",
"common": "workspace:^"
}
} service/tsconfig.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"references": [
{ "path": "../common" }
]
} service/src/index.ts
Create an Injector, set up an in-memory store for todos, and start the REST
server:
import { Injector } from '@furystack/inject'
import { addStore, InMemoryStore } from '@furystack/core'
import { useLogging, VerboseConsoleLogger } from '@furystack/logging'
import { getRepository } from '@furystack/repository'
import { JsonResult, useRestService } from '@furystack/rest-service'
import type { MyApi, TodoItem } from 'common'
const injector = new Injector()
useLogging(injector, VerboseConsoleLogger)
addStore(injector, new InMemoryStore({
model: Object as unknown as { new(): TodoItem },
primaryKey: 'id',
}))
getRepository(injector).createDataSet(
Object as unknown as { new(): TodoItem },
'id',
)
let nextId = 1
await useRestService<MyApi>({
injector,
port: 3000,
root: 'api',
name: 'My FuryStack Service',
api: {
GET: {
'/todos': async ({ injector: i }) => {
const dataSet = getRepository(i).getDataSetFor(
Object as unknown as { new(): TodoItem },
'id',
)
const entries = await dataSet.find(i, {})
return JsonResult(entries)
},
'/todos/:id': async ({ injector: i, getUrlParams }) => {
const { id } = getUrlParams()
const dataSet = getRepository(i).getDataSetFor(
Object as unknown as { new(): TodoItem },
'id',
)
const [entry] = await dataSet.find(i, { filter: { id: { $eq: id } } })
return JsonResult(entry)
},
},
POST: {
'/todos': async ({ injector: i, getBody }) => {
const body = await getBody()
const todo: TodoItem = {
id: String(nextId++),
title: body.title,
completed: false,
}
const dataSet = getRepository(i).getDataSetFor(
Object as unknown as { new(): TodoItem },
'id',
)
await dataSet.add(i, todo)
return JsonResult(todo)
},
},
},
})
console.log('Service listening on http://localhost:3000') 4. Frontend — Shades UI
The frontend uses Shades, FuryStack’s JSX-based UI library, bundled with Vite.
frontend/package.json
{
"name": "frontend",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -b && yarn vite build",
"start": "yarn vite --port 8080"
},
"dependencies": {
"@furystack/inject": "^12.0.0",
"@furystack/shades": "^13.0.0",
"@furystack/rest-client-fetch": "^8.1.0",
"common": "workspace:^"
},
"devDependencies": {
"vite": "^7.3.0"
}
} frontend/tsconfig.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"jsx": "react-jsx",
"jsxImportSource": "@furystack/shades"
},
"include": ["src"],
"references": [
{ "path": "../common" }
]
} frontend/index.html
Vite needs an HTML entry point in the workspace root:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My FuryStack App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html> frontend/src/index.tsx
Initialize the Shades root and render a simple todo list that fetches data from the service:
import { Injector } from '@furystack/inject'
import { createComponent, initializeShadeRoot } from '@furystack/shades'
import { TodoApp } from './components/todo-app.js'
const shadeInjector = new Injector()
const rootElement = document.getElementById('root') as HTMLDivElement
initializeShadeRoot({
injector: shadeInjector,
rootElement,
jsxElement: <TodoApp />,
}) frontend/src/components/todo-app.tsx
A Shade component that uses @furystack/rest-client-fetch for type-safe API calls:
import { createComponent, Shade } from '@furystack/shades'
import { createClient } from '@furystack/rest-client-fetch'
import type { MyApi, TodoItem } from 'common'
export const TodoApp = Shade({
customElementName: 'todo-app',
render: ({ useState, element }) => {
const client = createClient<MyApi>({
endpointUrl: 'http://localhost:3000/api',
})
const [todos, setTodos] = useState<TodoItem[]>('todos', [])
const [input, setInput] = useState('input', '')
const loadTodos = async () => {
const result = await client({
method: 'GET',
action: '/todos',
})
setTodos(result)
}
void loadTodos()
const addTodo = async () => {
if (!input) return
await client({
method: 'POST',
action: '/todos',
body: { title: input },
})
setInput('')
void loadTodos()
}
return (
<div>
<h1>Todos</h1>
<div>
<input
value={input}
oninput={(e: Event) =>
setInput((e.target as HTMLInputElement).value)}
placeholder="What needs to be done?"
/>
<button onclick={addTodo}>Add</button>
</div>
<ul>
{todos.map((todo) => (
<li>{todo.title}</li>
))}
</ul>
</div>
)
},
}) 5. Build and run
From the monorepo root, install dependencies and build:
yarn install
yarn build Then start the service and the frontend in separate terminals:
# Terminal 1
yarn start:service
# Terminal 2
yarn start:frontend
Open http://localhost:8080/
— you should see the todo app. Adding items calls the service API, which stores them in
memory via InMemoryStore.
What’s next?
You now have a working full-stack FuryStack application. From here you can:
-
Swap
InMemoryStoreforFileSystemStoreorMongodbStoreto persist data -
Add authentication with
@furystack/auth-jwtand@furystack/security - Add authorization rules to your data sets via
@furystack/repository - Explore the Getting Started hub for guided deep-dives into each package