Learning Rust: My First Steps with HackQuest’s Eclipse Track

ESTHER NAISIMOIESTHER NAISIMOI
11 min read

So, I recently signed up for the HackQuest Eclipse track, and that’s when I decided to take the plunge and start learning Rust.

At first? Smooth sailing! The theoretical part made sense, and I was like, “Okay, I got this.”

Then... came the technical part gosh and suddenly,

What the hell?!
became my daily catchphrase 😅😂

What Wasn’t Too Bad:

Not everything was scary, though. Some things were pretty digestible early on:

  • Rust’s syntax (it's actually pretty neat)

  • Basic data types

  • Compiling and building the project

  • Setting up the project root directory

  • Using comments

  • Printing outputs

  • Declaring variables

At that point, I was still doing okay ,feeling confident, even.

Then Came the Deep Technical Stuff:

Now this is where the real test began.

  • Advanced data types

  • Modules

  • Ownership, borrowing, lifetimes... oh my 😩

Trust me, without a solid grasp of these, things fall apart fast. Especially if you’re coming from a language where memory management isn’t something you think about all the time.


📌 Quick Side Note on Strings in Rust:

Starting off with datatypes:

  1. strings

Rust has two kinds of strings, and it took me a second to really get it:

  • StringOwned (Heap-allocated, mutable)

  • &strBorrowed (Immutable reference to a string slice)

The only visual clue?
One starts with a capital letter (String) and the other with a & symbol (&str)which actually tells you a lot about how ownership and borrowing work in Rust.

So the syntax?

here we go ……

//String

let you_name:String=”essie is my name”.to_string(); //similar to

let you_name:String=String::from(“essie is my name”");

//&string

let name_literal: &str = "Essie"; // this is string slice(its a string that can only be accessed but not modified)

let name_owned: String = String::from("Essie"); // owned String

Integers & Floating-Point Numbers in Rust

Rust provides two main categories of numeric types: integers and floating-point numbers.

  • Integers are whole numbers (positive, negative, or zero) like i32, u8, i64(side note:the i,u rep—> signed and unsigned types=signed(i) can store both positive and negative values the other(u) can only store positive values, including zero)

  • Floating-point numbers are used to represent numbers with decimal points, such as 3.14 or -0.5.

Rust supports two floating-point types:

  • f32 — 32-bit floating-point

  • f64 — 64-bit floating-point (default)

📌 Note: The number (e.g., 32 or 64) refers to the bit precision, not your CPU type. While f64 is the default and generally preferred for higher precision, Rust doesn’t automatically choose based on your CPU—it’s about performance and accuracy trade-offs.

Modules in Rust (Yes, They're a Thing!)

At first, I thought "Rust doesn’t really have modules like Python" LOL.
Well, plot twist: it does. They're just called modules, and they work a bit differently from what you might be used to in Python.

What Are Modules in Rust?

theyre just like a function(reausaable code) and are more about organizing and structuring your code but declared differently in contrast to functions with the keyword mod and curly braces.

Think of a module as a container for related functions, structs, enums, and other items. It helps you break your code into logical, manageable parts.

🔑 Key Points:

  • Declared using the mod keyword.

  • Use curly braces {} to define their scope.

  • Not automatically public —>you must use pub to expose contents.

  • Can be nested or split across files for better organization.

  •   mod  myfirst_module{ // // 'myfirst_module' is the module name
           pub myfunct(){//'myfunct' is a function defined inside 'myfirst_module'
              println!("Hello from the greetings module!");
          }   
      }
    
//You can then call it from main() like this:
fn main(){
    myfirst_module::myfunct(); // myfirst_module is a module then :: accesses items inside the function myfunc
//'::' is the path operator used to access items (like functions) inside the module 
}

📌 Don’t forget the pub keyword without it, myfunct() is private and won’t be accessible from main() or anywhere else.

If you were importing a module from a different crate (like a library you installed via Cargo.toml), then you'd use:

use other_crate::some_module;

before you can use a module or function from another crate (external library) in your Rust project, you first need to add the crate to your Cargo.toml file.

How to Use an External Crate in Rust

📝 Step 1: Add the crate to Cargo.toml

Let’s say you want to use the popular rand crate for generating random numbers.

In your Cargo.toml:

[dependencies]
rand = "0.8"  # or whatever the latest version is .his tells Cargo to download and compile the crate from crates.io the next time you build your project.

Step 2: Using the crate in your Rust file

In your main.rs:

use rand::Rng; // Rng is a trait inside the rand crate

fn main() {
    let mut rng = rand::thread_rng();
    let n: u8 = rng.gen_range(1..=100);
    println!("Random number: {}", n);
}

bring the whole crate of rand vs bringing the only triats you need?

Bring only what you need into scope is considered best practice, and here’s why:

Bringing the whole crate

use rand;

fn main() {
    let mut rng = rand::thread_rng();
    let n: u8 = rand::Rng::gen_range(&mut rng, 1..=10);
}

Bringing only the trait you need

use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let n: u8 = rng.gen_range(1..=10);
}

TL;DR – Why You Should Only Import What You Need in Rust

✅ 1. Clarity & Readability

Using something like use rand::Rng; makes it immediately clear what you're working with. No guesswork, it’s explicit and self-documenting.

✅ 2. Avoid Namespace Pollution

Importing the entire crate (use rand;) brings in everything ;even things you don’t need. This can lead to name collisions and clutter, especially in large or multi-crate projects.

✅ 3. Better Autocompletion & Docs

IDE tools like VS Code and IntelliJ work better with targeted imports. You’ll get cleaner autocomplete, better error messages, and easier-to-follow documentation.

✅ 4. It’s Idiomatic Rust

Minimal, precise imports are part of what makes Rust code clean and maintainable. It reflects Rust’s core values: explicitness, modularity, and fine-grained control.

✨ Bottom line: Import what you use ; and nothing more.

📚 Common Rust Crates & Their Traits

CratePurposeNotable Traits
randRandom number generationRng, SeedableRng
serdeSerialization/deserializationSerialize, Deserialize
tokioAsync runtimeAsyncRead, AsyncWrite, Stream
futuresAsync programming primitivesFuture, Stream
regexRegular expressionsRegex (struct with methods)
chronoDate and time handlingTimeZone, DateTime (structs & traits)

🔍 What Are Traits?

Traits in Rust define shared behavior that types can implement.

example

What is the Deserialize Trait?

The Deserialize trait comes from the serd``e crate (short for Serialize/Deserialize).
It allows Rust structs and enums to be converted from data formats like JSON into usable Rust types.

First: Add serde and serde_json to your Cargo.toml

[dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"

Using Deserialize

use serde::Deserialize;

Step 2: Create a struct and derive Deserialize

#[derive(Debug, Deserialize)]
struct User {
    name: String,
    age: u8,
}

Step 3: Deserialize JSON into the struct

use serde_json;

fn main() {
    let data = r#"{"name": "Essie", "age": 22}"#;

    let parsed: User = serde_json::from_str(data).unwrap();

    println!("Name: {}, Age: {}", parsed.name, parsed.age);
}

TL;DR – What Deserialize Does

TraitPurpose
DeserializeConverts external data → Rust
  • 📥 Takes formats like JSON, TOML, YAML

  • 🧱 Maps them to Rust structs/enums

  • ⚙ Requires the struct to #[derive(Deserialize)]


💡 Bonus

You can use serde::Serialize together with Deserialize to do both directions:

rustCopyEdit#[derive(Serialize, Deserialize)]
struct User { ... }

If you're diving into Solana development using Rust, you're entering one of the most advanced and exciting use cases for the language. Solana smart contracts (called programs) are written in Rust, and they require mastery over some core Rust modules and Solana-specific crates.

Here’s a list of must-know Rust modules & concepts to get you started on the right foot:

🦀 Must-Know Rust Modules & Crates for Solana Development

🔧 1. std::vec, std::string, std::collections

  • These are foundational.

  • You'll work with Vec<u8>, String, and sometimes HashMap.

Why? Solana programs work with binary data (Vec<u8>) and serialized inputs.


🗃️ 2. borsh or serde (for serialization/deserialization)

  • Solana programs often require you to serialize/deserialize instruction data or account state.
tomlCopyEditborsh = "0.10"
serde = { version = "1.0", features = ["derive"] }
  • Traits to know:

    • BorshSerialize, BorshDeserialize

    • Serialize, Deserialize


🧱 3. solana_program crate

  • The official SDK for Solana programs.
tomlCopyEditsolana-program = "1.18"  # (match your Solana CLI version)

Includes:

  • entrypoint! macro – defines your program's entrypoint

  • ProgramResult, AccountInfo, Pubkey – core types

  • msg! – logging within Solana

  • invoke / invoke_signed – cross-program invocations


🔐 4. solana_sdk crate (for local testing or clients)

  • Used more in clients or tests than inside the program.

  • Includes keypair generation, transactions, signatures, etc.


📦 5. anyhow, thiserror – for cleaner error handling

tomlCopyEditanyhow = "1.0"
thiserror = "1.0"
  • Helps create descriptive, debuggable errors instead of Box<dyn Error>

🧪 6. solana-program-test

  • Simulates a local Solana cluster for testing your smart contracts off-chain.

  • Works with tokio and async code.


🔄 Bonus: Core Rust Concepts You Must Know

ConceptWhy It Matters in Solana Dev
Ownership & BorrowingAccounts are references—misuse = compile error
LifetimesWhen using references inside structs/enums
EnumsFor instruction types (like SPL Token program)
TraitsFor serialization, logging, etc.
MacrosUsed in entrypoint!, msg!, etc.
Result/Error HandlingMust return ProgramResult on failure

TL;DR?

Cont’' : Rust Modules(some repeated)

These come from the standard library (std) and are essential for any Rust development especially Solana:

🔹 Foundational Modules

  1. std::vec::Vec – Dynamic arrays (e.g. Vec<u8>)

  2. std::string::String – Text data

  3. std::collections::HashMap – Key-value storage

  4. std::result::Result – Error handling

  5. std::option::Option – Handling nullable values

  6. std::convert – For Into, From, etc.

  7. std::mem – For manipulating memory layout

  8. std::cmp – Comparisons (e.g. PartialEq, Ord)

  9. std::fmt – Formatting, custom Display

  10. std::io – (Used more off-chain, but useful for CLI tools used to get user input )


📦 Solana-Specific Crates

These are the official and community crates you’ll encounter in Solana dev:

🔹 Solana Program Development

  1. solana_program – Core SDK for on-chain programs

    • Includes types like AccountInfo, Pubkey, entrypoint!, invoke
  2. solana_sdk – Used for client-side development (keypairs, RPC, etc.)

  3. solana_program_test – Local in-Rust test environment (simulates a cluster)

  4. solana_client – RPC client to interact with Solana from off-chain apps

  5. solana_clap_utils – CLI utilities for building wallets or clients

  6. solana_transaction_status – Used to fetch and parse tx info from RPC


🔄 Serialization / Deserialization

Solana smart contracts almost always involve data encoding.

  1. borsh – Binary Object Representation Serializer for Hashing

    • Use #[derive(BorshSerialize, BorshDeserialize)]
  2. serde – General-purpose serialization

  3. serde_json – Use with serde for JSON APIs (mostly off-chain)

  4. serde_bytes – Efficient binary serialization via serde


⚙️ Smart Contract Patterns & Utils

Useful tools that make writing contracts easier.

  1. thiserror – For clean, enum-based error messages

  2. anyhow – Flexible error handling (better for off-chain/client-side)

  3. num_derive / num_traits – For converting between types

  4. bytemuck – Zero-cost casting for byte slices

  5. arrayref – For working with byte arrays in account data

  6. base64 – Encoding/decoding (used in some off-chain RPC workflows)


🧪 Testing & Tooling

  1. tokio – Async runtime for client-side interactions or testing

  2. assert_matches – For precise assertions in unit tests

  3. dotenvy / dotenv – Load .env configs for local dev/testing

  4. clap – Build CLI tools that interact with Solana programs


🧩 Bonus: Optional But Helpful

  • spl_token – Solana Program Library's token program bindings

  • solana-metadata – Used in NFT development

  • anchor_lang – If you're using the Anchor framework (high-level abstraction over Solana)


✅ TL;DR Summary Table

CategoryKey Crates or Modules
Core RustVec, String, Result, Option, fmt
Solana Coresolana_program, solana_sdk, borsh
Serializationborsh, serde, serde_json
Testing/Clientstokio, solana_program_test, dotenvy
Utilsthiserror, arrayref, num_traits

Learning Rust for Solana development is a journey filled with challenges, breakthroughs, and those priceless “aha!” moments and I’d genuinely love to hear about yours too.

Whether you're just getting started or deep into building on-chain programs, your experience might help someone else take the next step.

🗣️ Drop your thoughts, questions, or even frustrations in the comments let’s learn together!

P.S. If you’re brand new to Rust, I recommend starting with the W3Schools Rust Introduction it's beginner-friendly and a great way to grasp the fundamentals quickly before diving deeper.

10
Subscribe to my newsletter

Read articles from ESTHER NAISIMOI directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

ESTHER NAISIMOI
ESTHER NAISIMOI