Managing Resources
Embed custom files into your Tauri v2 application and access them at runtime using the Resources API
The Resources API lets you bundle arbitrary files—JSON configurations, images, default documents—directly into your Tauri application. Those files become part of the compiled binary and are always available, no network request or separate installation needed. This page covers how to add resources, how to reach them from Rust and from your React frontend, and how to read different file types safely.
Why bundle resources instead of using the public folder
Files placed in the public directory of a Vite project get served as static assets during development, but Tauri’s build process does not automatically carry them into the final application binary. The Resources API exists so you can ship files that are not part of the web UI but still belong with the application: machine‑learning model weights, a default configuration file that the app reads on first launch, or templates used to generate reports.
The key difference is that resources are embedded into the binary and extracted at runtime. You control which files get included, they live outside the webview’s asset tree, and you can access them from both Rust and JavaScript through dedicated APIs.
Adding resources to your project
Adding resources means telling the Tauri bundler which files to include and where to place them. The process is sequential—if you miss a step, the files won’t appear in the final binary.
Step 1: Configure the resource list
Open src-tauri/tauri.conf.json and add a resources array inside the bundle section. Each entry is a glob pattern relative to the project root (the folder that contains src-tauri). The Configuration: Resources chapter covers the same bundle.resources key from the config side.
// src-tauri/tauri.conf.json
{
"bundle": {
"resources": [
"resources/**/*"
]
}
}
The pattern resources/**/* picks up every file and folder inside a directory named resources at the project root. You can use multiple patterns or target specific extensions—for example, "resources/config/*.json".
Resource paths are relative to the project root:
Patterns like "../some-folder/**/*" or absolute paths are not supported. Keep your resource directory next to src-tauri or use a sub‑path that stays within the project root.
Step 2: Place the files you want to embed
Create the folder structure that matches your patterns. For the configuration above, add a resources folder at the project root and put your files inside.
my-tauri-app/
├── src-tauri/
│ └── tauri.conf.json
├── resources/
│ ├── config/
│ │ └── defaults.json
│ └── templates/
│ └── report.docx
├── src/
└── package.json
The root of the resource bundle becomes the resources directory itself. A file at resources/config/defaults.json will later be reachable as config/defaults.json—the resources/ prefix is not part of the runtime path.
Step 3: Build and verify
Run npm run tauri build (or cargo tauri build). The resources are embedded automatically; there is no extra compilation step. You can confirm everything worked by checking the binary size—embedding a few megabytes of resources will show a noticeable increase.
No runtime configuration needed:
Once the build succeeds, the files are part of your application. You never need to copy them manually or worry about install paths.
Accessing resources from Rust
Rust code can reach embedded resources in two complementary ways. The choice depends on whether you need a filesystem path or just want to read the bytes.
Resolving a path gives you an absolute filesystem location. Tauri extracts resources to a temporary directory at startup, and BaseDirectory::Resource points there. You can then use standard std::fs to read, copy, or pass the path to other libraries.
// src-tauri/src/main.rs
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let path = app.path().resolve(
"config/defaults.json",
tauri::path::BaseDirectory::Resource,
)?;
let content = std::fs::read_to_string(&path)?;
println!("Resource contents: {}", content);
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
The call to resolve() takes a relative path that starts from the root of the resource bundle. Because we placed resources/config/defaults.json in the project, the relative path is config/defaults.json. The method returns a PathBuf on success, and any IO error (file missing, permission denied) is surfaced normally.
Path resolution extracts the file to disk:
Behind the scenes, Tauri extracts the embedded resource file to a temporary directory the first time it is needed. This means the file exists as a normal filesystem entry, and you can open it with any tool that expects a path—a command line tool, a database engine, or an archive library.
Which method you pick is largely about the final consumer: if a Rust library expects a Path, use path resolution. If you just want to read and parse the content inside your own code, the resource handle is simpler and often faster.
Accessing resources from the React frontend
Tauri serves all bundled resources over its built‑in asset protocol. In Tauri v2 the protocol uses the https://asset.localhost origin. Any file in the resource bundle is available under a URL that matches its relative path.
// src/components/ResourceLoader.tsx
import { useState, useEffect } from 'react';
function ResourceLoader() {
const [content, setContent] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('https://asset.localhost/config/defaults.json')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.text();
})
.then((text) => setContent(text))
.catch((err) => setError(err.message));
}, []);
if (error) return <div className="error">Failed to load resource: {error}</div>;
if (content === null) return <div>Loading…</div>;
return <pre>{content}</pre>;
}
export default ResourceLoader;
The URL https://asset.localhost/config/defaults.json mirrors the relative path we used earlier. No resources/ prefix appears in the URL—the asset protocol considers the root of the resource bundle as its root.
For binary files like images, fetch the response as a blob and create an object URL.
// src/components/ImageResource.tsx
import { useState, useEffect } from 'react';
function ImageResource() {
const [imageUrl, setImageUrl] = useState<string | null>(null);
useEffect(() => {
fetch('https://asset.localhost/templates/logo.png')
.then((res) => res.blob())
.then((blob) => setImageUrl(URL.createObjectURL(blob)))
.catch(console.error);
// Clean up the object URL when the component unmounts
return () => {
if (imageUrl) URL.revokeObjectURL(imageUrl);
};
}, []);
return imageUrl ? <img src={imageUrl} alt="Embedded resource" /> : null;
}
export default ImageResource;
Asset protocol only works inside Tauri:
Calling fetch('https://asset.localhost/...') from a regular browser will fail. During development with npm run tauri dev, the webview already runs inside Tauri, so the protocol works as expected. If you need to debug the frontend in a browser without Tauri, use a fallback that loads from public/ instead.
Reading bundled files: text, JSON, and binary
The mechanics of reading a resource are the same regardless of file type. What changes is how you interpret the bytes.
Plain text — use response.text() on the frontend, std::fs::read_to_string or String::from_utf8 in Rust. Always handle the possibility that the file is not valid UTF‑8; a danger note appears below about this exact pitfall.
JSON configuration — fetch the text and parse it. On the Rust side, serde_json::from_slice(&bytes) works directly on the resource bytes. On the frontend, response.json() does the same.
Binary files (images, documents, models) — on the frontend, use response.blob() or response.arrayBuffer(). In Rust, the bytes returned by the resource handle are already Vec<u8>; you can feed them straight into a library like image or zip.
// Reading a binary resource in Rust (using the resource handle)
if let Some(mut resource) = app.resource().open("templates/report.docx") {
let mut doc_bytes = Vec::new();
resource.read_to_end(&mut doc_bytes)?;
// doc_bytes can now be passed to a document processing crate
}
Text encoding assumptions cause subtle bugs:
Both std::fs::read_to_string and String::from_utf8 assume the file uses UTF‑8. If you embed a file saved in Windows‑1252 or ISO‑8859‑1, the conversion will fail silently or produce garbled output. Always verify the encoding of your resource files, or use a crate like encoding_rs when you cannot control the source.
Structuring resources for maintainability
As an application grows, the resources folder tends to become a dumping ground. A disciplined structure prevents path confusion later.
- Group by purpose:
config/,templates/,assets/models/,assets/samples/. - Keep a README inside the resource folder that lists which files are required and why. This helps anyone who later modifies the patterns.
- Avoid duplication with the frontend
publicdirectory. If a file is needed only by the web UI, keep it inpublicand let Vite bundle it. If Rust also needs it, put it in resources. - Pin exact file paths in globs when you can.
resources/config/*.jsonis safer thanresources/**/*because it won’t accidentally pick up editor swap files or.DS_Storeartifacts.
Resource files are read‑only at runtime:
The extracted resource directory is not meant for writing. If your application needs to modify a file after reading it, copy the resource to the app’s data directory first using the filesystem API, then work on the copy.
Common mistakes when working with resources
Several errors surface only after a production build. The ones below are the most frequent.
- Wrong relative path after extraction. A file at
resources/static/guide.htmlis reachable asstatic/guide.html, notresources/static/guide.html. Theresources/prefix is stripped because the bundle root is theresourcesfolder itself. Test the path after a build to be sure. - Glob does not match newly added files. The
bundle.resourcespatterns are evaluated at build time. If you add a file but the glob is too narrow (e.g.,"resources/config/defaults.json"only matches that exact file), the new file is silently ignored. Use"resources/config/*"if you expect to add more config files later. - Fetching from the wrong origin during development. The asset protocol is
https://asset.localhost, nothttp://localhost:1420. A fetch tohttp://localhost:1420/resources/...will hit the Vite dev server, which doesn’t have those files unless you also placed them inpublic. - Assuming resources are available without a Tauri build. The
fetchcalls only work inside a Tauri webview. Unit tests that run in Node.js or in a regular browser will fail—mock the fetch or skip the test.
Summary
Embedding files through the Resources API gives you a reliable way to ship data that belongs with the application but isn’t part of the web UI. The same relative path works in Rust (with either path resolution or a resource handle) and in the frontend through the asset protocol. The biggest trap is getting the path right: the bundle root starts inside your resources directory, so drop the resources/ prefix at runtime.
Once you can reliably read a bundled file, you’ll often want to write data back—for user settings, logs, or generated reports.