Tauri Project Structure Deep Dive - Frontend, Backend & Configuration Explained
Video: Tauri Project Structure Deep Dive - Frontend, Backend & Configuration Explained by Taught by Celeste AI - AI Coding Coach
Two halves, one bundle.
src/for the frontend,src-tauri/for the Rust backend,tauri.conf.jsonfor 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"]
}
}
productNameandversion— show up in the bundle and the OS.identifier— bundle identifier. Reverse-domain-name format.build.beforeDevCommand— run beforetauri dev(typicallynpm run dev).build.devUrl— the URL to point the webview at during development.build.frontendDist— where the production build lives (relative tosrc-tauri).app.windows— initial window configuration: title, size, decorations, transparent, etc.bundle— output formats and icons fortauri 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:
- Frontend calls
invoke('save_note', { title, body }). - Tauri runtime (inside the Rust process) routes the call to the matching
#[tauri::command]function. - The Rust function runs (saves the file, queries a DB, whatever).
- The return value is serialised (via serde) and sent back to the frontend.
- The frontend receives the resolved value of the
invokepromise.
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.