---
title: Web apps
description: Rules for TanStack Start apps, currently apps/web.
---

Top level: `components/`, `features/`, `functions/`, `hooks/`, `libs/`, `providers/`, `routes/`, `widgets/`. `@/` points at `src/`. File and class rules are in [Frontend](/development/frontend).

## Features

A feature in `features/<names>/` uses only the layers it needs. Push logic down and keep UI dumb:

| Layer                | Holds                                                                   |
| -------------------- | ----------------------------------------------------------------------- |
| `api/`               | Query contracts shared by loaders and pages, such as `definePageQuery`. |
| `libs/<domain>/`     | Pure code without React: transforms, parsing, query keys, types.        |
| `hooks/`             | Stateful logic without markup.                                          |
| `providers/`         | Context and runtime wiring. Never depends on widgets.                   |
| `components/<name>/` | Presentational parts. Data and callbacks come through props only.       |
| `widgets/<region>/`  | Composed regions that call data hooks, including single-use dialogs.    |
| `pages/`             | Route entries.                                                          |

- Each layer exposes an `index.ts`; a feature has no root barrel. Outside a feature, import its layer barrel (`@/features/errors/hooks`); inside, import relatively.
- `functions/isomorphic/` holds `.isomorph.ts` modules, `functions/server/` holds `.server.ts` ones. Client code deep-imports a `.server.ts` file instead of the server barrel.
- Route-only loaders and search schemas stay in `routes/`. A feature's root `constants.ts` and `types.ts` hold only symbols several layers share.
- Keep a helper, constant or type next to its only consumer. Constants used by one function live inside it; static arrays, objects and Lingui descriptors live at module scope. Derive result types with `ReturnType` next to their owner.
- Load shared data in the closest common parent and pass it down, instead of hooks that only forward it.
- Tests sit next to the code whose behaviour they check.
- Name mutation hooks `use<Entity><Action>`. Components, hooks and `*QueryOptions` are exempt from verb-first names.

## Dialogs

- Open a dialog with a detached trigger: `useState(() => Dialog.createHandle<Payload>())`, a `Dialog.Trigger` with that `handle` and a `payload`, and one `Dialog` or `DialogConfirm` reading `payload` from its render prop.
- Don't control dialogs with `open` and `onOpenChange`. The mobile sidebar is the one exception.

## Settings

| What is edited                          | How it saves                                                                         |
| --------------------------------------- | ------------------------------------------------------------------------------------ |
| An independent text field               | `InlineTextField`: on blur or Enter, Esc reverts, errors under the field.            |
| An independent switch, select, checkbox | On change, disabled while saving, errors in a toast through `meta.errorTitle`.       |
| Fields that only make sense together    | One form with `UnsavedChangesBar`, `useUnsavedChanges` and `useBlockUnsavedChanges`. |
| A new entity or a list item             | An explicit Create or Save button, in a dialog or on its own page.                   |
| A destructive action                    | The danger zone, behind `DialogConfirm`.                                             |
| A state action: enable, mute, sync      | A button or menu in the page header.                                                 |

Fields show no saving state of their own. A form with text fields confirms each save with a success toast that has one `id` per form, so repeated saves replace it; lists of standalone switches skip it. Creating, deleting and state actions toast their result, and a new entity opens after it's created.
