Resource Best Practices
Organizing, naming, and accessing bundled resources in Tauri v2 applications with React and Vite, ensuring cross-platform compatibility and maintainability.
Resources are files you bundle inside your Tauri application — images, configuration files, pre-trained models, or any data your app needs at runtime without downloading. How you organize, name, and reference these files determines whether your app works on the first launch or breaks silently on someone else’s machine. A few straightforward habits eliminate nearly all resource-related issues. Contrast this with frontend assets, which go through Vite.
Organizing Resources
A chaotic resource directory turns simple path lookups into a guessing game. The default resource location in a Tauri project is the src-tauri/resources/ folder. Tauri v2 automatically includes everything inside this directory when you build — no extra configuration required. If that folder does not exist, you can create it or point the bundler to a different location through tauri.conf.json.
Group files by purpose rather than dumping everything into one flat folder. A clear structure makes it obvious where a file lives and reduces the chance of name collisions as the app grows.
src-tauri/
└── resources/
├── config/
│ ├── settings.json
│ └── feature-flags.json
├── i18n/
│ ├── en.json
│ └── de.json
└── models/
└── classifier.onnx
If your project is small, a flat directory is fine. But once you have more than a handful of files, a few subdirectories keep the mental model straightforward.
Custom resource paths:
You can add files from outside the default resources directory by listing them in tauri.conf.json under bundle > resources. Each entry can be a path or a glob pattern. Files added this way are copied to the same virtual root, so be mindful of potential name conflicts.
Resource Naming
A file named My Data.json might work perfectly on your development machine and then fail on a teammate’s Linux system or in CI. File naming rules are not about style; they are about predictability across operating systems.
Use lowercase letters, digits, hyphens, and underscores. Hyphens (-) are preferred over underscores in URLs because they are more readable and do not get hidden by underlines in links, but both are safe. The critical rule is to avoid spaces and special characters. A space in a filename forces you to percent-encode it in URLs, and not every tool handles that the same way.
Stick to kebab-case for multi-word filenames: app-settings.json, not appSettings.json or app_settings.json. Consistency makes it easier to remember the exact name when you are writing fetch calls.
Case sensitivity will bite you:
macOS and Windows filesystems are case-insensitive by default; Linux is case-sensitive. If you reference config.json but the file is actually named Config.json, it will work during development on a Mac and then throw a 404 in production on Linux. Always test your builds on a Linux target — a Docker container with a Ubuntu image is enough — to catch these mismatches.
Extensions matter too. Use .json, .png, .onnx consistently. Avoid uppercase extensions (settings.JSON) because some asset protocol implementations or MIME type detections may not recognise them.
Relative Paths and the Resource Protocol
Every resource your app bundles is served through a custom protocol. The webview accesses bundled files at asset://localhost/, and the path is always relative to the resource root — not to your frontend source directory or the file system root of the device. This is the mental model that makes everything click.
If your resource directory looks like this:
src-tauri/resources/
└── data/
└── config.json
you fetch it from the frontend as:
fetch('asset://localhost/data/config.json')
On Windows, the protocol host may appear as http://tauri.localhost/, but using asset://localhost works uniformly across all platforms because Tauri normalises it internally.
In Rust, you resolve a resource path with the tauri::Manager trait and the BaseDirectory::Resource constant. This gives you the absolute filesystem path, regardless of whether you are in development or a packaged build.
use tauri::Manager;
use std::fs;
#[tauri::command]
fn read_config(app: tauri::AppHandle) -> Result<String, String> {
let resource_path = app
.path()
.resolve("data/config.json", tauri::BaseDirectory::Resource)
.map_err(|e| e.to_string())?;
fs::read_to_string(resource_path).map_err(|e| e.to_string())
}
The same relative path string — data/config.json — is used in both environments. The separation of concerns is clean: Tauri decides where the resource actually lives on disk; your code only knows the logical path.
One path, everywhere:
When you define a logical resource path like icons/logo.png and use it in both Rust and the frontend, you have a single source of truth. If you later restructure the actual resource folder, you adjust the bundler mapping — not the code.
Cross-Platform Compatibility
Tauri targets Windows, macOS, and Linux. Each platform handles file paths, line endings, and file size limits differently. A few defensive habits prevent build failures and runtime crashes.
Path Separators
Always use forward slashes (/) when writing resource paths in tauri.conf.json, Rust code, or fetch URLs. Tauri normalises these internally on Windows, so data/config.json works everywhere. Using backslashes (\) works on Windows during development but breaks on macOS and Linux builds.
Line Endings in Text Resources
If you bundle plain-text configuration files and later read them in Rust with fs::read_to_string, the result will contain the platform’s native line endings (\r\n on Windows, \n everywhere else). Parsing logic that expects a specific line separator should normalise the content after reading — for example, by replacing \r\n with \n — or use a library that handles this transparently.
File Size and Package Size
Every file you put into resources becomes part of the application bundle. A 500 MB machine‑learning model will make the installer huge and slow down every update. Keep resources lean, and consider downloading large, rarely‑used files on first launch instead of bundling them.
Symbolic Links and Shortcuts
Do not rely on symlinks inside the resource directory to point to files outside the project tree. The bundler may not follow symlinks consistently across platforms, especially on Windows. Always place the actual file inside the resource folder or add it explicitly through bundle > resources in the configuration.
Permissions and Executable Resources
If you bundle shell scripts or helper executables, remember that on macOS and Linux the execution bit must be set. Tauri preserves file permissions during the build, but if you add a script on Windows and then build for Linux in CI, the permission bit may be lost. Test packaged builds on their target platform.
Accessing Resources from React
From the frontend, the asset://localhost protocol behaves like a standard HTTP server: you can fetch JSON, reference images in src attributes, or load binary files.
import { useState, useEffect } from 'react';
interface AppConfig {
appName: string;
apiEndpoint: string;
}
function App() {
const [config, setConfig] = useState<AppConfig | null>(null);
useEffect(() => {
fetch('asset://localhost/config/settings.json')
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((data) => setConfig(data))
.catch((err) => console.error('Resource load failed:', err));
}, []);
if (!config) return <p>Loading configuration…</p>;
return (
<div>
<h1>{config.appName}</h1>
<p>API: {config.apiEndpoint}</p>
</div>
);
}
export default App;
Never embed secrets in resources:
Anything inside the resource folder is accessible to the webview, which means any JavaScript running in your app can read it. API keys, database passwords, or private keys bundled as resources are effectively public. Keep secrets in environment variables or fetch them from a backend service after launch.
For images, you can set src directly:
<img src="asset://localhost/icons/logo.png" alt="Logo" />
A Practical Setup Walkthrough
If you are adding resources to a project for the first time, this sequence covers the entire flow.
Create the resource directory
Inside the src-tauri folder, create a resources directory. This is the default location Tauri v2 looks for.
mkdir src-tauri/resources
Add your files
Place files into the directory using a subfolder structure that reflects their logical paths. For example, create config/settings.json and icons/logo.png.
{
"appName": "MyApp",
"apiEndpoint": "https://api.example.com"
}
Verify bundler configuration
The default resources field in tauri.conf.json is empty, meaning Tauri uses the default directory. If you need files from elsewhere, add them explicitly.
{
"bundle": {
"resources": {
"config/*": "./extra-resources/"
}
}
}
When the default directory already contains everything you need, you can leave this section as is.
Access the resource from the frontend
Use the asset://localhost protocol with the logical path relative to the resource root.
import { useState, useEffect } from 'react';
function App() {
const [appName, setAppName] = useState('');
useEffect(() => {
fetch('asset://localhost/config/settings.json')
.then((res) => res.json())
.then((data) => setAppName(data.appName));
}, []);
return <h1>{appName || 'Loading...'}</h1>;
}
export default App;
After running cargo tauri dev, you should see your application name rendered from the bundled JSON file.
Common Pitfalls
These are the mistakes that appear most often in real-world Tauri projects. Spotting them early saves hours of debugging.
- Using a relative path that starts with
./or../. The webview’s origin is not the file system; it does not understand directory traversal. Always start the path from the resource root:asset://localhost/config.json, neverasset://localhost/../config.json. - Assuming every file in the resource folder is automatically included. The default directory
src-tauri/resourcesis included, but if you place files anywhere else without adding them tobundle > resources, they will not be bundled. - Forgetting to handle loading states in the frontend. Resources are available immediately in production builds, but during development they load asynchronously. A component that assumes the data is ready will crash the first render. Always guard with a loading state or an error boundary.
- Not testing on a case‑sensitive filesystem. If your CI only runs on macOS or Windows, case mismatches will sneak through. Add a Linux build step to your pipeline.
- Bundling files that change frequently without a cache‑busting strategy. Resources are baked into the app binary or installer. Updating them requires a new release. If a configuration file needs to change outside of a full app update, store it in the app data directory instead and fall back to the bundled resource as the default.
The most common starter mistake:
Calling fetch('config.json') without the protocol and expecting it to resolve relative to the frontend’s source folder. The browser inside Tauri treats this as a request to https://tauri.localhost/config.json (or the equivalent), which will fail because the development server does not serve that file. Always use the full asset://localhost/... URL.
Summary
Treat resources as a small, static file server embedded inside your app. A consistent directory layout, kebab-case filenames, and forward‑slash relative paths ensure the same code works on every platform without surprises. The asset://localhost protocol is the single interface both Rust and the frontend agree on — using it exclusively and avoiding raw filesystem paths keeps the entire resource pipeline predictable.
When your resource needs grow beyond a handful of configuration files, the same principles extend cleanly: subdirectories map to logical URL segments, Rust commands resolve the same relative strings, and platform differences are abstracted away.