Skip to content

Project Anatomy — Cargo.toml, src/, and the Module System

Playground note: Project anatomy inherently involves multiple files and directory structure — it cannot be demonstrated in a single-file browser playground. Run the examples locally with cargo new and cargo run.

When you run cargo new my-app, Cargo creates a minimal but complete project:

flowchart TD
  root["my-app/"] --> toml["Cargo.toml — project manifest (= package.json)"]
  root --> lock["Cargo.lock — pinned dependency versions (= package-lock.json)"]
  root --> src["src/"]
  src --> main["main.rs — entry point (= src/index.ts)"]
What cargo new gives you (binary crate)

For a library (no main, just an API for other crates to use):

Terminal window
cargo new --lib my-lib
flowchart TD
  root["my-lib/"] --> toml["Cargo.toml"]
  root --> src["src/"]
  src --> lib["lib.rs — library entry point (= index.ts that only exports)"]
Library crate layout
TypeScript
// package.json
{
"name": "my-app",
"version": "1.0.0",
"description": "A Node.js app",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"express": "^4.18.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/express": "^4.17.0"
}
}
Rust
# Cargo.toml
[package]
name = "my-app"
version = "0.1.0"
edition = "2021" # Rust edition (like target in tsconfig)
# No separate devDependencies:
# [dependencies] = runtime deps
# [dev-dependencies] = test-only deps
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1"

Key differences:

  • There is no scripts section. You run cargo run, cargo test, etc. directly.
  • edition corresponds to the Rust language edition (2015, 2018, 2021). Always use 2021 for new projects.
  • Dependencies do not need @types/ packages. Rust types are always part of the crate.
  • Feature flags (features = ["derive"]) are Rust’s equivalent of optional peer dependencies or configuration flags. They let crates expose additional APIs that are compiled in only when requested.

A binary crate has src/main.rs. A library crate has src/lib.rs. You can have both in the same project — src/lib.rs contains the shared logic, src/main.rs calls into it.

flowchart TD
  root["my-app/"] --> src["src/"]
  src --> main["main.rs — fn main() { ... }"]
  src --> lib["lib.rs — pub fn library_code() { ... }"]
  src --> utils["utils.rs — a module (pub mod utils;)"]
  src --> models["models/"]
  models --> mod["mod.rs — module root (or models.rs in Rust 2018+)"]
  models --> user["user.rs — pub struct User { ... }"]
A project with modules

In TypeScript you use import/export. In Rust you use mod to declare a module and use to bring items into scope:

TypeScript
// TypeScript ESM
// src/utils.ts
export function add(a: number, b: number): number {
return a + b;
}
// src/main.ts
import { add } from './utils';
console.log(add(1, 2));
Rust
// src/utils.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// src/main.rs
mod utils; // declares the module (looks for src/utils.rs)
use utils::add;
fn main() {
println!("{}", add(1, 2));
}

When you run cargo build, Rust compiles everything into target/. This is the equivalent of dist/ plus node_modules/.cache/. You never commit it to version control.

flowchart TD
  root["target/"] --> debug["debug/ — cargo build (fast compile, no optimisation)"]
  debug --> dbin["my-app — the executable"]
  root --> release["release/ — cargo build --release (slower compile, fast binary)"]
  release --> rbin["my-app"]
The target/ directory

Add target/ to your .gitignore. Cargo does this automatically when you run cargo new.

In JavaScript, npm install downloads packages into node_modules/ inside your project. In Rust, cargo add <crate> downloads packages (called crates) into a global registry cache at ~/.cargo/registry/. Your project’s target/ directory contains compiled artifacts, but the source is shared across all projects on your machine.

The global cache means:

  • No per-project duplication of the same crate
  • cargo clean removes target/ but keeps the download cache
  • Building the same version of a crate twice is nearly instant (already compiled)

Like package-lock.json, Cargo.lock pins the exact versions of every transitive dependency. For binary projects, commit Cargo.lock. For libraries published to crates.io, add it to .gitignore — just like you would not commit package-lock.json in a published npm package.

What is the Rust equivalent of package.json?
Where does Rust store compiled build artifacts?
What does `mod utils;` do in Rust?
For a published Rust library crate, should Cargo.lock be committed?