Back to Knowledge Base

Data Structures in Web Development

Sep 19, 2024
KK
Shivam MathurSenior Developer

In my perspective, when it comes to creating a web product, everything revolves around data. Views request and display data to the user, the server interlinks different sources of data, modifies it, and sends it elsewhere, while a database stores and retrieves data. This perspective has served me well in my work, allowing me to understand my clients’ systems quickly by focusing on the flow of data. It also eliminates unnecessary implementation details, helping me concentrate on what’s important.

A strong understanding of data structures is essential. How can we understand what’s happening if we can’t recognise the data and how it’s being modified?

Here, I’ll list the data types and structures I encounter the most, along with their implementations in Rust.

Structuring Data

There are myriad ways to structure data, allowing us to manage it in ways that best serve its use case.

Primitives

Understanding primitives is important as these are the building blocks of complex JSON objects found everywhere.

// Basic primitives
let logical: bool = true; // Boolean
let a_float: f64 = 1.0; // Float
let integer: i64 = -100; // Int
let unsigned_integer: u64 = 100; // Unsigned Int
let char_type: char = 'a'; // Rarely used outside of strings

There are some types that are not primitives but are often used similarly.

let string_slice: &str = "String slice"; // String slice
// All emails, passwords, user input fields start as strings
// before they are validated and parsed into other types
let string: String = String::from("String"); // String
// Sometimes BigInt is used for large values like ID fields in large databases
// Requires the num_bigint crate in Rust
let bigint: BigInt = BigInt::parse_bytes("22405534230753963835153736737".as_bytes(), 10).unwrap();

Compound Data

Data grouped together with some rules.

// Fixed length of same data type on definition
let array: [i64; 4] = [1, 2, 3, 4];
// Tuple fixed length, multiple data types
let tuple: (i64, char) = (1, 'a');

Collections

More complex groupings and implementations of data structures.

Vector

Useful for single-dimensional data with varying length. It can also be used as Stack and Queue data structures.

// Vector of same data type with variable length
let mut vector: Vec<i64> = vec![1, 2, 3, 4];
// Can be used as a stack
vector.push(7); // [1, 2, 3, 4, 7]
let removed_value = vector.pop(); // [1, 2, 3, 4]
// Can also be used as a queue using std::collections::VecDeque;
let mut vec_queue: VecDeque<i64> = vec![1, 2, 3, 4];
// Remove from queue
let front = vec_queue.pop_front(); // [2, 3, 4]
// Add to queue
vec_queue.push_back(5); // [2, 3, 4, 5]

Set Useful when we want to ensure all values are unique.

// Unique values of the same type
let mut set: HashSet<i64> = HashSet::from([1, 2, 3, 4]);
set.insert(5); // [1, 2, 3, 4, 5]
set.insert(2); // [1, 2, 3, 4, 5] Duplicate not added
set.remove(&4); // [1, 2, 3, 5]
set.contains(&1); // true

Map Useful for storing data to process multiple times without needing to iterate through the entire list to find items.

// Key-value pair of fixed types
let mut map: HashMap<&str, Vec<i32>> = HashMap::new();
map.insert("key1", vec![1, 2, 3]);
let val_get = map.get("key1").cloned();
let val_remove = map.remove("key1");

Buffer

All data is a series of 1s and 0s. When we want to work with a string, we tell the program to interpret that data as a string. That’s why in most languages, when doing I/O, we’ll have an initial step of putting the received data into a Buffer before converting it to a usable format.

use std::fs::File;
use std::io::{self, BufRead};

fn main() -> std::io::Result<()> {
    let file = File::open("test.txt")?;
    let reader = io::BufReader::new(file);

    for line in reader.lines() {
        println!("{}", line?);
    }
    Ok(())
}

I had done a small DNS client to understand how DNS works using NodeJS. That also required the use of Buffers.

	createSocket(ip,callback)
    {
	    // ...
        socket.on("message", (buffer) => {  
	        let packet = new DNSPacket(buffer);
        // ...
        }
        //...
    }
    resolve(domain,record)
    {
	    serverIps.forEach(ip=>{
	    // ...
            this.servers[ip].socket.send(packet.toBuffer(),...,(err, bytes) => {
            // ...
            })
        })
    }

Complex Types

We can define our own complex types at any point and define the behavior of the data. The sky’s the limit; we can make this as complex or as simple as required.

struct Person {
    name: String,
    age: u8
}

impl Person {
    fn new(name: String, age: u8) -> Self {
        Person {
            name,
            age
        }
    }

    fn talk(&self) -> String {
        format!("Hi I'm {0} and I'm {1} years old!", self.name, self.age)
    }
}

fn main() {
    // Create a new person data
    let shivam = Person::new(String::from("Shivam"), 29);
    // Person data can talk
    println!("{0}", shivam.talk()); // Hi I'm Shivam and I'm 29 years old!
}

Data Mutation

There is a lot of nuance when it comes to modifying data. We could be mutating the data directly, working on the data in a way that creates new modified data while keeping the original data immutable, or doing a mix of both. Everything depends on the specific use case.

Mutable Data

Let’s add a system that registers a birthday event for a person to the previous implementation of the Person struct.

impl Person {  
    fn birthday(&mut self) -> String {  
        self.age += 1;  
        format!("{0} Happy Birthday to me!", self.talk()) // Sad  
    }  
}  
  
fn main() {  
    // Create a new person data  
    let mut shivam = Person::new(String::from("Shivam"), 29);  
    // Person data can talk  
    println!("{0}", shivam.talk()); // Hi I'm Shivam and I'm 29 years old!  
    // Person is a year older  
    // Hi I'm Shivam and I'm 30 years old! Happy Birthday to me!  
    println!("{0}", shivam.birthday());   
}

This sad birthday implementation will mutate the existing data for the Person data in the variable shivam.

Immutable Data

Immutable means that the data being operated on is not modified while performing an operation. In this example, I have removed the sad birthday method and introduced a happy child birth event.

// Use of lifetimes to specify that parent data should exist until the lifetime of the child  
struct Person<'a> {  
    name: String,  
    age: u8,  
    parents: Option<(&'a Person<'a>, &'a Person<'a>)>  
}  
impl Person<'_> {  
    fn new(name: String, age: u8) -> Self {  
        Person { name, age, parents: None }  
    }  
    fn talk(&self) -> String {  
        match self.parents {  
            Some((father, mother)) =>   
                format!(  
                        "Hi I'm {0} and I'm {1} years old! \  
                        My parents are {2} and {3}.",  
                        self.name,  
                        self.age,  
                        father.name,  
                        mother.name  
                ),  
            None =>   
                format!(  
                        "Hi I'm {0} and I'm {1} years old!",  
                        self.name,  
                        self.age  
                )  
        }  
    }  
    fn child<'a>(  
        father: &'a Person<'a>,  
        mother: &'a Person<'a>,  
        child_name: String  
    ) -> Person<'a> {  
        Person {  
            name: child_name,  
            age: 0,  
            parents: Some((father, mother))  
        }  
    }  
}  
fn main() {  
    // Create new person data  
    let bunty = Person::new(String::from("Bunty"), 29);  
    let bubbly = Person::new(String::from("Bubbly"), 29);  
    println!("{0}", bunty.talk()); // Hi I'm Bunty and I'm 29 years old!  
    println!("{0}", bubbly.talk()); // Hi I'm Bubbly and I'm 29 years old!  
    // Immutable event (existing data doesn't change, new data is created)  
    // Two people can have a child  
    let bubbles = Person::child(&bunty, &bubbly, String::from("Bubbles"));  
    // Hi I'm Bubbles and I'm 0 years old! My parents are Bunty and Bubbly.  
    println!("{0}", bubbles.talk());  
}

Here, the parent data is not modified in any way, and a new child is created with references to the parent Person data.

Note: We have also created a Binary Tree data structure here since it fits to describe this use case.

Mixed

We don’t need to fix our systems to one single style. We can use mutability and immutability where it fits.

This implementation looks pretty confusing due to the nature of the Rust borrow checker. We need to manage references and lifetimes.

use std::{cell::RefCell, rc::Rc};

#[derive(Debug)]
struct Person {
    name: String,
    age: u8,
    parents: Option<(Rc<RefCell<Person>>, Rc<RefCell<Person>>)>
}

impl Person {
    fn new(name: String, age: u8) -> Self {
        Person {
            name,
            age,
            parents: None
        }
    }

    fn birthday(&mut self) -> String {
        self.age += 1;
        format!("{0} Happy Birthday to me!", self.talk()) // Sad
    }

    fn talk(&self) -> String {
        match &self.parents {
            Some((father, mother)) => {
                format!("Hi I'm {0} and I'm {1} years old! My parents are {2} and {3}.", self.name, self.age, father.borrow().name, mother.borrow().name)
            },
            None => format!("Hi I'm {0} and I'm {1} years old!", self.name, self.age)
        }
    }

    fn child(father: Rc<RefCell<Person>>, mother: Rc<RefCell<Person>>, child_name: String) -> Person {
        Person {
            name: child_name,
            age: 0,
            parents: Some((father, mother))
        }
    }
}

fn main() {
    // Create a new person data
    let bunty = Rc::new(RefCell::new(Person::new(String::from("Bunty"), 29)));
    let bubbly = Rc::new(RefCell::new(Person::new(String::from("Bubbly"), 29)));
    // Person data can talk
    println!("{0}", bunty.borrow().talk()); // Hi I'm Bunty and I'm 29 years old!
    println!("{0}", bubbly.borrow().talk()); // Hi I'm Bubbly and I'm 29 years old!
    // Immutable event (existing data doesn't change, new data is created)
    // Two people can have a child
    let mut bubbles = Person::child(Rc::clone(&bunty), Rc::clone(&bubbly), String::from("Bubbles"));
    // Hi I'm Bubbles and I'm 0 years old! My parents are Bunty and Bubbly.
    println!("{0}", bubbles.talk());

    // And so a year goes by
    // Hi I'm Bunty and I'm 30 years old! Happy Birthday to me!
    println!("{0}", bunty.borrow_mut().birthday());
    // Hi I'm Bubbly and I'm 30 years old! Happy Birthday to me!
    println!("{0}", bubbly.borrow_mut().birthday());
    // Hi I'm Bubbles and I'm 1 year old! My parents are Bunty and Bubbly. Happy Birthday to me!
    println!("{0}", bubbles.birthday());
    println!("{:#?}", bubbles);
    // Person {
    //     name: "Bubbles",
    //     age: 1,
    //     parents: Some(
    //         (
    //             RefCell {
    //                 value: Person {
    //                     name: "Bunty",
    //                     age: 30,
    //                     parents: None,
    //                 },
    //             },
    //             RefCell {
    //                 value: Person {
    //                     name: "Bubbly",
    //                     age: 30,
    //                     parents: None,
    //                 },
    //             },
    //         ),
    //     ),
    // }
}

In this implementation, we mix mutable and immutable data. While creating new data, we can reference existing data without modifying it, ensuring safe and predictable data manipulation.

Conclusion

Understanding data structures is key to effective web development. Each type, from primitives to custom types, impacts application efficiency and performance. Focusing on data flow and manipulation helps design robust, scalable solutions.

A solid grasp of data structures ensures efficient, maintainable code and optimized performance. Incorporating these concepts enhances technical skills and aids in navigating complex systems. Keep exploring and experimenting to find the best fit for each challenge.

article
coding
data-structures
rust
software-development
tutorials
web-development