Structuring a Rust Codebase - Exploring Rust's Module System for Code Organization
Understand the Rust Modules system by building an authentication service.
Introduction
Structuring a Rust codebase can be so frustrating for beginners. I experienced this issues multiple times while starting out new projects in Rust. Typically, I default to the hexagonal architecture while building a web project in Golang, but replicating in Rust has not been exactly straight forward.
In this article, we will explore the Rust’s module system in detail. We will cover packages, crates and modules, and how best to structure them.

Adding visibility to components
When you start out building software that has different parts of services, you will probably create different folders to house specific services or parts.
Golang Example
In Golang for example, an authentication service codebase can look like (bare-bones project here):
- authentication-service/
|- main.go
|- go.mod
|- api/
|- handlers.go
|- routes.go
|- authentication/
|- user.go
|- token.go
|- repository.go
|- service.go
|- storage/
|- database.go
|- utils/
|- encryption.goapi/routes.go
package api
import (
"github.com/gorilla/mux"
)
func SetupRouter() *mux.Router {
router := mux.NewRouter()
router.HandleFunc("/signup", SignupHandler).Methods("POST")
router.HandleFunc("/login", LoginHandler).Methods("POST")
return router
}main.go
api/routes.go
package main
import (
"github.com/theghostmac/authService/api"
"log"
"net/http"
)
func main() {
router := api.SetupRouter()
log.Fatal(http.ListenAndServe(":8080", router))
}This works without any errors because of the visibility rule in Go. These folders are called modules, or tiniest units of software that can be executed or used by themselves.
A single source file can be a module, so we can actually put theroutes.go
SetupRouter()
- removing the unused import ,“github.com/theghostmac/authService/api”
- renaming topackage api.package main
Now, I believe you’ve refreshed your memory on modules. Let’s see how this applies to Rust.
Rust implementation with Rocket
Say a Rust developer wants to recreate the authentication service with Rust, they would write all the functions and type declaration in thesrc/main.rs
Writing tests in for a simple Rust function is as simple as scrolling down in that same source file and using thecompiler attributes.#[test]
However, from normal software engineering best practices, the Single Responsibility Principle in the SOLID principles teach that “each micro service should have a single responsibility or focus on one specific business capability.”
Having a standalonemain.rs
#[macro_use] extern crate rocket;use diesel::prelude::*;use std::env;use rocket::response::status::Created;use rocket::response::status::Unauthorized;mod authentication {use std::fmt::Error;use super::*;
migrations
Modules in Rust
Modules in Rust are similar to those in Go. You can create custom modules (or folders, if you like) to serve different purposes. You can also leave single source files in thesrc/
main.rs
-
Create three directories inside of calledsrc/,modules, andentities. Create a submodule insiderepositoriesdirectory calledmodules.authentication
-
Create three files: inuser.rsdirectory,entitiesinauthentication.rsdirectory underauthentication, andmodulesinuser_repository.rsdirectory.repositories
-
Create a file in the three directories. Addmod.rsto thepub mod <file name without .rs extension>files, e.g. inmod.rs, theentitiesfile while have onlymod.rsin it. The codebase structure will look like:pub mod user;
src/ ├── entities │ ├── mod.rs │ └── user.rs ├── main.rs ├── modules │ ├── authentication.rs │ └── mod.rs └── repositories ├── mod.rs └── user_repository.rs -
Add the following code to each:
// user.rspub struct User {pub id: i32,pub username: String,pub password: String,}impl User {pub fn new(id: i32, username: String, password: String) -> Self {Self {id,username,password,}}}// authentication.rsuse crate::entities::user::User;use crate::repositories::user_repository::UserRepository;pub struct AuthenticationModule {user_repository: UserRepository,}impl AuthenticationModule {pub fn new(user_repository: UserRepository) -> Self {// user_repository.rsuse crate::entities::user::User;pub struct UserRepository {}impl UserRepository {pub fn new(database_url: &str) -> Self {// Create and initialize the necessary database connection or ORM instance -
Update the file with the following:main.rs#[macro_use]extern crate rocket;use diesel::prelude::*;use rocket::response::status::Created;use rocket::response::status::Unauthorized;use std::env;use dotenv::dotenv;mod entities;
Putting it all together
Great! Now that we have discussed the necessary steps for creating new modules in our mini hexagonal application, let's put everything together.
To recap, here are the key points:
-
The new modules should be placed inside the directory.src/
-
Each module should have a file that exports the main module file. However, there are two alternative approaches you can take:mod.rs
-
Option 1: Directly create the module file under : Instead of creating a separatesrc/file, you can create themod.rsfile (or any other module file) directly under theuser.rsdirectory. In this case, you would include the module using thesrc/keyword in your main application file (mod). For example, let's assume we have amain.rsmodule. To include it in your application, you would add the following line in youruserfile:main.rsThis approach eliminates the need for a separatemod user;file, as the module file is directly placed in themod.rsdirectory.src/
-
Option 2: Create a file in the new module: Alternatively, you can create amod.rsfile within the new module directory and include the intended contents of themod.rsfile in it.user.rs
Here's an example file structure for better understanding:
In thesrc/ |- main.rs |- user/ |- mod.rs |- user.rsfile inside themod.rsmodule directory, you would include the following code:userThis code exports thepub mod user;module, making it accessible from other parts of the application.user
-
Option 1: Directly create the module file under
-
Once you have created the modules and included them in your application, you can start using them by calling their functions, structs, or traits.
For example, let's assume you have defined a function namedin thecreate_usermodule. You can use it in youruserfile like this:main.rsMake sure to import the necessary items from the modules using themod user;use user::user::create_user;fn main() {// Call the create_user function from the user modulecreate_user("John Doe", "john.doe@example.com");}keyword.use
Using cargo-modules
to visualize
The cargo-modules
cargo-modules
cargo install cargo-modulesCargo.toml
[dependencies]
cargo-modules = "0.9.1"To use it, run:
cargo modules generate treeThe output for the demo authentication service is:
cargo modules generate tree
crate authservice_rs
├── mod entities: pub(crate)
│ └── mod user: pub
├── mod modules: pub(crate)
│ └── mod authentication: pub
└── mod repositories: pub(crate)
└── mod user_repository: pub--types
cargo module generate tree --types
crate authservice_rs
├── mod entities: pub(crate)
│ └── mod user: pub
│ └── struct User: pub
├── mod modules: pub(crate)
│ └── mod authentication: pub
│ └── struct AuthenticationModule: pub
└── mod repositories: pub(crate)
└── mod user_repository: pub
└── struct UserRepository: pubConclusion
By organizing your code into separate modules, you can maintain a more structured and modular codebase. Each module can focus on a specific functionality or domain, improving code readability, reusability, and maintainability.
That's it! You now have a mini hexagonal application with multiple modules organized within thesrc/
Happy coding!