Creating a Tauri project
Step 2 of 13, and it is the step that turns the map into a running thing. You start with an empty
directory and finish with a native window on screen — a real macOS, Windows or Linux window, not a
browser tab — without having written a line of your own code yet. That is not a gimmick: the
scaffold Tauri gives you is the skeleton every later step edits, so it is worth reading what
create-tauri-app actually generated before step 4 starts changing it.
The commands here were run on macOS with Node 22 and Rust 1.99.0. On Windows or Linux everything is the same except the window decorations.
Create the project
Section titled “Create the project”create-tauri-app is the official scaffold tool. Run it through npm so you don’t have to install
anything globally:
npm create tauri-app@latest rustnote -- --manager npm --template react-ts --identifier com.rustnote.app --yesThe flags replace the interactive prompts: --manager npm wires the front end to npm, --template react-ts picks React with TypeScript (the track assumes React and does not teach it), and
--identifier com.rustnote.app sets the application identifier that the bundle step in step 12
bakes into the installers — changing it later is fiddly, so pick it now. --yes skips the
remaining prompts. Without the flags the tool asks the same questions one at a time.
What comes back:
> npx> create-tauri-app rustnote --manager npm --template react-ts --identifier com.rustnote.app --yes
Template created! To get started run: cd rustnote npm install npm run tauri android init npm run tauri ios init
For Desktop development, run: npm run tauri dev
For Android development, run: npm run tauri android dev
For iOS development, run: npm run tauri ios dev(The version numbers in npm’s own wrapper lines vary with what’s current; this run used
create-tauri-app 4.8.0. Ignore the Android and iOS lines — this track is desktop only.)
rustnote is now a directory. Everything in this track — including the examples repository tags —
lives directly inside it, so cd rustnote and stay there.
The three directories
Section titled “The three directories”Strip out the build output (node_modules/, dist/, src-tauri/target/ — all gitignored) and the
project is this:
.├── .vscode│ └── extensions.json├── public│ ├── tauri.svg│ └── vite.svg├── src│ ├── assets│ │ └── react.svg│ ├── App.css│ ├── App.tsx│ ├── main.tsx│ └── vite-env.d.ts├── src-tauri│ ├── capabilities│ │ └── default.json│ ├── icons│ │ ├── 128x128.png│ │ ├── 32x32.png│ │ └── …│ ├── src│ │ ├── lib.rs│ │ └── main.rs│ ├── .gitignore│ ├── build.rs│ ├── Cargo.lock│ ├── Cargo.toml│ └── tauri.conf.json├── .gitignore├── index.html├── package.json├── package-lock.json├── README.md├── rust-toolchain.toml├── tsconfig.json├── tsconfig.node.json└── vite.config.ts(README.md and rust-toolchain.toml are this track’s additions — the second one is explained
below. The icons directory holds 15 generated files in every size the bundle step needs; three
stand for the rest.)
Three directories, one per layer:
src/— the React front end. A Vite project, nothing Tauri-specific about it:index.htmlloadssrc/main.tsx, which renderssrc/App.tsx. The template page shows the Tauri, Vite and React logos and a greet box that calls the template’s demo Rust command — step 4 replaces that demo with the app’s own commands.src-tauri/— the Rust half. A complete cargo crate with its ownCargo.toml, its own.gitignore, andtauri.conf.json, which is where Tauri’s own behaviour is configured.public/— static files Vite copies into the bundle as-is: logos, later the app’s own assets.
Everything else at the top level belongs to the front-end toolchain (package.json,
tsconfig.json, vite.config.ts). The rule of thumb worth keeping for the whole track: if a file
sits under src-tauri/, it is Rust-side; if not, it is front-end.
The Rust half
Section titled “The Rust half”src-tauri/Cargo.toml is worth reading in full, because it holds two decisions the template made
for you:
[package]name = "rustnote"version = "0.1.0"description = "A Tauri App"authors = ["you"]edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]# The `_lib` suffix may seem redundant but it is necessary# to make the lib name unique and wouldn't conflict with the bin name.# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519name = "rustnote_lib"crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]tauri-build = { version = "2", features = [] }
[dependencies]tauri = { version = "2", features = [] }tauri-plugin-opener = "2"serde = { version = "1", features = ["derive"] }serde_json = "1"
# Read the optimization guideline for more details: https://tauri.app/concept/size/#cargo-configuration[profile.release]codegen-units = 1lto = trueopt-level = 3panic = "abort"strip = trueTwo things to notice. First, the project is a crate that is both a library and a binary:
[lib] names it rustnote_lib with the staticlib/cdylib crate types so mobile builds can link
it as a static or dynamic library, and src/main.rs is a three-line binary that just calls
rustnote_lib::run(). Second, the edition is 2024 — what a current cargo new produces, and what
this track’s Rust code assumes throughout.
The source is two small files. src-tauri/src/main.rs:
// Prevents additional console window on Windows in release, DO NOT REMOVE!!#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() { rustnote_lib::run()}And src-tauri/src/lib.rs, which is where the app is actually assembled:
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/#[tauri::command]fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name)}
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application");}greet is the template’s demo command; steps 4–11 grow real ones in this file. The
tauri_plugin_opener line is likewise a template extra — it powers the “click a logo” links on
the demo page, and step 9’s file-system work introduces the plugins this track actually needs.
The configuration file
Section titled “The configuration file”src-tauri/tauri.conf.json is Tauri’s own config, separate from both package.json and
Cargo.toml:
{ "$schema": "https://schema.tauri.app/config/2", "productName": "rustnote", "version": "0.1.0", "identifier": "com.rustnote.app", "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://localhost:1420", "beforeBuildCommand": "npm run build", "frontendDist": "../dist" }, "app": { "windows": [ { "title": "rustnote", "width": 800, "height": 600 } ], "security": { "csp": null } }, "bundle": { "active": true, "targets": "all", "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" ] }}The build section is the glue between the two halves: in dev, Tauri starts the Vite dev server
(beforeDevCommand), then loads the app from http://localhost:1420 (devUrl); for a release
build it runs npm run build and ships the files in ../dist instead. The app.windows entry is
why the window is 800×600 with the title rustnote. The one file not shown here is
src-tauri/capabilities/default.json, which lists what the webview is allowed to do — it stays
boilerplate until step 9.
One addition: pin the Rust version
Section titled “One addition: pin the Rust version”create-tauri-app does not pin a Rust version, so the project builds against whatever toolchain
your machine defaults to. The Rust side of this track is written and tested against Rust 1.99.0
with edition 2024, so add one file at the project root — rust-toolchain.toml, next to
package.json:
[toolchain]channel = "1.99.0"With that file present, any cargo or tauri command inside the project uses 1.99.0 (rustup
downloads it once if you don’t have it), and your build matches the article’s instead of drifting
with your default toolchain.
Install and run
Section titled “Install and run”npm installThis run installed @tauri-apps/api@2.12.4, @tauri-apps/cli@2.12.3, react@19.3.1 and
vite@8.3.4, among 27 packages — your minor versions may be slightly newer; that’s expected and
harmless here. Then start the app:
npm run tauri devThe first run compiles the whole Rust side — around 340 crates. On an M2 Max that took 2 minutes 14 seconds; a slower machine will take longer, and it is a one-off. What you see:
Running BeforeDevCommand (`npm run dev`)> vite VITE v8.3.4 ready in 285 ms
Running DevCommand (`cargo run --no-default-features --color always --`) Info Watching /path/to/rustnote/src-tauri for changes... Compiling rustnote v0.1.0 (/path/to/rustnote/src-tauri) Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.58s Running `target/debug/rustnote`(The Info Watching and Compiling lines carry your own project’s path; the timings vary. Cargo’s
Building [...] progress lines are trimmed here.)
Read it top to bottom and the whole architecture is already visible: Tauri starts Vite
(BeforeDevCommand), then runs cargo run in src-tauri (DevCommand), the compiled binary
opens a native window and loads the front end from localhost:1420. A rustnote window appears —
the template page with the three logos and the greet box. Quit the app (close the window) and the
dev process exits with it.
Run npm run tauri dev again after that first build and the Rust side finishes in about a second;
only files you actually change get recompiled, and both halves reload on save. That loop is what
steps 3–11 live inside.
Where the code stands
Section titled “Where the code stands”This step’s end state is tagged tauri-ch02 in
the examples repository. If your project misbehaves,
git checkout tauri-ch02 in your clone puts your tree exactly where this article left it, and a
diff against your own files shows which line drifted.
You have a window, and you have seen from the dev output that it is two processes talking. Step 3 unpacks that: what the front end, the IPC boundary and the Rust core each own, and why Tauri doesn’t ship a browser engine with your app. The track overview keeps the full list current as articles land.