Skip to content
Select theme

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-tauri-app is the official scaffold tool. Run it through npm so you don’t have to install anything globally:

Terminal window
npm create tauri-app@latest rustnote -- --manager npm --template react-ts --identifier com.rustnote.app --yes

The 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.

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.html loads src/main.tsx, which renders src/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 own Cargo.toml, its own .gitignore, and tauri.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.

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/8519
name = "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 = 1
lto = true
opt-level = 3
panic = "abort"
strip = true

Two 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.

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.

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.

Terminal window
npm install

This 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:

Terminal window
npm run tauri dev

The 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.

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.