Build from Scratch

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 InMemoryStore for FileSystemStore or MongodbStore to persist data
  • Add authentication with @furystack/auth-jwt and @furystack/security
  • Add authorization rules to your data sets via @furystack/repository
  • Explore the Getting Started hub for guided deep-dives into each package