Back to Blog

Tauri Project Structure Deep Dive - Frontend, Backend & Configuration Explained

Sandy LaneSandy Lane

Video: Tauri Project Structure Deep Dive - Frontend, Backend & Configuration Explained by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Two halves, one bundle. src/ for the frontend, src-tauri/ for the Rust backend, tauri.conf.json for the rules.

Once you have a Tauri project running, the next thing is to know your way around it. Tauri's directory layout is small but specific — everything is in one of three places: the frontend, the Rust backend, or the config file that wires them together.

The top-level layout

A scaffolded Tauri 2 project looks like:

my-app/
├── src/                  # frontend (React, Vue, Svelte, vanilla — your choice)
├── src-tauri/            # Rust backend
├── public/               # static assets (favicon, etc.)
├── index.html
├── package.json
├── vite.config.ts        # frontend bundler config
└── ... (tsconfig, .gitignore, etc.)

Two sub-projects coexist. The frontend is a normal Vite/React/TS project — it could be deployed to a web host, by itself, with no changes. The backend is a normal Cargo project that depends on tauri = "2".

The wiring happens at build time. When you run npm run tauri dev, Tauri starts both halves and points the Tauri window's webview at the Vite dev server.

The frontend

src/
├── App.tsx               # main React component
├── main.tsx              # React entry: ReactDOM.render(<App />)
├── index.css             # global styles
└── components/           # your components

Same structure as any Vite + React project. Tauri imposes no convention here — use your favourite library, framework, state manager, styling approach. The only Tauri-specific code is when you call invoke('command_name', { args }) to talk to Rust, but that's an import from @tauri-apps/api, not a structural requirement.

The backend

src-tauri/
├── Cargo.toml            # Rust dependencies
├── tauri.conf.json       # Tauri configuration
├── build.rs              # build script (calls tauri-build)
├── icons/                # app icons for each platform
├── capabilities/         # security capability files (Tauri 2 ACL)
└── src/
    ├── main.rs           # entry: calls run()
    └── lib.rs            # the actual application

A Cargo project. The lib.rs / main.rs split is conventional — main.rs is a thin shim that calls lib.rs::run(). This split exists so Tauri can build for mobile (which uses a different entry-point macro) without duplicating logic.

Cargo.toml lists tauri as the main dependency, tauri-build as a build dependency, and any plugins (tauri-plugin-fs, tauri-plugin-dialog, etc.). Standard Rust crates work too — serde, regex, chrono, uuid, anything from crates.io.

tauri.conf.json

The configuration file. Lives in src-tauri/tauri.conf.json.

Key sections:

{
  "productName": "My App",
  "version": "0.1.0",
  "identifier": "com.you.my-app",
  "build": {
    "beforeDevCommand": "npm run dev",
    "beforeBuildCommand": "npm run build",
    "devUrl": "http://localhost:1420",
    "frontendDist": "../dist"
  },
  "app": {
    "windows": [
      {
        "title": "My App",
        "width": 800,
        "height": 600
      }
    ]
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": ["icons/32x32.png", "icons/128x128.png"]
  }
}
  • productName and version — show up in the bundle and the OS.
  • identifier — bundle identifier. Reverse-domain-name format.
  • build.beforeDevCommand — run before tauri dev (typically npm run dev).
  • build.devUrl — the URL to point the webview at during development.
  • build.frontendDist — where the production build lives (relative to src-tauri).
  • app.windows — initial window configuration: title, size, decorations, transparent, etc.
  • bundle — output formats and icons for tauri build.

The full schema has dozens of options. The defaults are sensible; you usually only override productName, identifier, and the window size.

Capabilities (Tauri 2 ACL)

src-tauri/capabilities/default.json

Tauri 2 introduced a permission system. Plugins (filesystem, dialog, HTTP, etc.) declare what they can do; your app's capabilities file declares which of those permissions you want to grant.

A typical capability file:

{
  "identifier": "main-capability",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:default",
    "dialog:default"
  ]
}

This says: the window named "main" gets the default permissions of the core, filesystem, and dialog plugins. Without this, the frontend can't call invoke('plugin:fs|read_text_file') even if the plugin is installed.

The model is opt-in: by default, your frontend can only call things you explicitly allow. Useful for security; mildly annoying when you forget to grant a permission.

How a request flows

User clicks a button in React. The flow:

  1. Frontend calls invoke('save_note', { title, body }).
  2. Tauri runtime (inside the Rust process) routes the call to the matching #[tauri::command] function.
  3. The Rust function runs (saves the file, queries a DB, whatever).
  4. The return value is serialised (via serde) and sent back to the frontend.
  5. The frontend receives the resolved value of the invoke promise.

That whole round trip is microseconds for trivial commands. For commands that block (file I/O, network), use async and Tauri will run them on a worker without blocking the UI.

What goes where

A few rules of thumb.

Frontend (src/): - All UI rendering. - Navigation, state management, animations. - Anything users see directly.

Backend (src-tauri/): - File I/O, network, database. - OS-specific APIs (clipboard, notifications, system tray). - Anything that can't or shouldn't run in a webview.

Config (tauri.conf.json): - Window properties. - Bundle metadata. - Plugin permissions (via capabilities).

Don't try to do filesystem work from the frontend with weird browser APIs. Don't try to render UI from Rust. Each side has its job.

Common mistakes

Editing the wrong package.json. There's one in the project root (frontend) and you might create one accidentally in src-tauri/. The frontend one is the one you usually edit.

Forgetting capabilities. Plugin commands fail silently from the frontend if the capability isn't granted. Check src-tauri/capabilities/default.json.

Hardcoding paths. Don't hardcode /Users/you/Documents. Use Tauri's path plugin to get OS-appropriate directories.

Putting business logic in main.rs. Keep main.rs thin. All the logic lives in lib.rs (or modules called from it).

What's next

Next lesson: commands. The #[tauri::command] macro and invoke() from the frontend — the bridge between the React side and the Rust side.

Recap

src/ is the frontend (whatever framework). src-tauri/ is the Rust backend with a lib.rs (logic) and main.rs (entry point). tauri.conf.json configures the app, the window, and the bundle. Capabilities (Tauri 2 ACL) declare what plugin features your frontend may use. Frontend renders UI; backend handles native operations; commands bridge them.

Next: commands. See you in the next one.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.