Skip to content

How to get into Rust as a TypeScript Developer

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

How to get into Rust as a JavaScript/TypeScript Developer

Rust is the language of my dreams. I work primarily as a JavaScript developer, so my skillset largely includes the web, as well as NodeJS environments. I love JavaScript, but I wanted a new language to dive into. That new language needed to check off a few boxes:

  • Rich Type System (TypeScript made me see the value of this)
  • Fast / Low Level (JavaScript is SO SLOW, I want speed next)
  • Great Documentation
  • Community

I originally thought my new language was going to be C++. It has the speed I wanted, and is a strongly typed language that is also statically typed. The big problem with C++, though, was that there wasn't any sign of official documentation, and the community seems a lot less beginner-friendly than I was hoping.

Then came Rust. Rust checks all the boxes. Speaking about their documentation alone, it is OUTSTANDING! It's a one-stop shop for all of my language needs. As a beginner, this is SUCH a time saver. Shout-out to mdbook for having one of the cleanest documentation formats I have ever seen.

The Speed of Rust

I briefly mentioned my desire for great documentation, but now, let's look at speed!

This blog post by Sean Wragg sums up the speed of Rust in a comparison of web frameworks. In his article, he compared Rocket (a web framework written in Rust) to Restify (another web framework written in NodeJS). He benchmarked how many requests per second each service was able to handle, and the results were staggering.

Rocket vs Restify Responses per Second

The requests/second each service could handle:

  • Restify: 7,996.19
  • Rocket: 72,133.75

Based on these benchmarking results, the Rocket-powered service was able to handle 9x the amount of requests than Restify. This is wild!

Let's dive into Rust and see how we can switch our trains of thoughts from JavaScript to Rust.

Prerequisites

We'll need to install Rust onto our computer for local Rust development. This process is incredibly easy. Navigate here and their documentation will give you a way to install Rust, tailored to whatever OS you use when you click that link.

Executing the install process will install the entire rust tool-chain onto your computer:

  • rustc: The Rust compiler, Usually not invoked directly, but instead, through cargo.
  • cargo: Think of this as a Rust's version of a super-powered npm. It downloads dependencies, compiles and runs our code, and can distribute our packages to the Rust Community Crate Registry.
  • rustup: This installs and updates Rust.

The most important component of this tool-chain is Cargo.

Your new npm, Cargo

From the Cargo Book

Cargo is the Rust package manager. Cargo downloads your Rust package's dependencies, compiles your packages, makes distributable packages, and uploads them to crates.io, the Rust community’s package registry. You can contribute to this book on GitHub.

We are going to mainly use 3 cargo commands:

  • cargo new
  • cargo build
  • cargo run

As a JavaScript developer, I feel right at home with this. It feels like I'm working with npm scripts in a package.json file.

Hello World

Let's finally create a Rust project. Run the following command to initialize a new package.

cargo new hello_world

Cargo new in VSCode

This will create our folder structure for the rust package, and create two important files:

  • Cargo.toml
  • main.rs

Think of Cargo.toml as Rust's version of package.json. The main file we're interested in, however, is the main.rs file. This file contains the main function- the starting point in all Rust programs.

fn main() {
    println!("Hello, world!");
}

In the span of a few seconds, we've already created our Hello World program. This is a great foundation with which we can begin diving deeper into Rust fundamentals.

Types

One of the tools in the JavaScript ecosystem that I fell in love with is TypeScript. There was recently a time I worked on a time-sensitive project, and there just was NOT the time to test the application as much as I wanted to.

I had chosen to use TypeScript instead of JavaScript to write out the front-end, and it caught SO many errors as I typed out my code. I felt confident that I understood the structure of all of my data and overall, I felt more confident in the codebase with it being strongly typed.

Here are some simple types from TypeScript:

const anInt: number = 17;
const aString: string = "Hello";
const aBoolean: boolean = true;
const anArray: number[] = [1, 2, 3, 4, 5];

The typing system of Rust feels just like writing TypeScript code. Here is a similar declaration of variables, but this time, in Rust.

let _an_int: i32 = 32;
let _a_string: &str = "Hello, world!";
let _a_bool: bool = true;
let _an_array: [i32; 3] = [1, 2, 3];

Writing this code gives me the same feel of writing TypeScript, a huge plus considering how much I love using it.

Structs and Enums

Let's check out a more complicated type. Let's say I wanted to declare the shape of an object representing a person. Here's an example of how I would implement that object type in TypeScript.

export interface Person {
    name: string;
    age: number;
    hobby: string;
    job_title: string;
}

Here is how I could implement a similar object using Rust structs.

#[derive(Debug)]
pub struct Person {
    pub name: String,
    pub age: i32,
    pub hobby: String,
    pub job_title: String,
}

Again, this gives me a very similar experience to writing TypeScript.

Let's instantiate an object of the Person type using Rust, and print it to standard output.

let me: Person = Person {
    name: "Matthew Pagan".to_string(),
    age: 32,
    hobby: "Coding".to_string(),
    job_title: "Software Developer".to_string(),
};

println!("{:?}", me);
Rust Development Environment

This is Rust, yet it's so similar to what I already use on a day-to-day basis. I love TypeScript, and Rust scratches that itch.

Let's implement a favorite color field to the Person struct, and restrict available colors by implementing a Color enum for that type.

Adding Color Enum

Options and Matching

Rust really starts to shine when we start working with logic that involves optional values, and how to handle optional data.

Let's expand our Person type. Here is how our Person type is currently defined:

use super::Color;

#[derive(Debug)]
pub struct Person {
    pub name: String,
    pub age: i32,
    pub hobby: String,
    pub job_title: String,
    pub favorite_color: Color,
}

Not all Person's have jobs. Maybe some of those objects represent a child who may not be old enough to enter the workforce, or someone older who has already retired.

TypeScript has one way of dealing with this scenario, using optional properties.

Here is how we could implement an optional job_title in a Person interface in TypeScript.

export interface Person {
    name: string;
    age: number;
    hobby: string;
    job_title?: string;
    favorite_color: Color;
}

In the above implementation, TypeScript says that job_title is either a string or null. Rust's solution is brilliantly elegant. There is no concept of null. Instead, we'll use the Option enum to handle the implementation.

Here is how we would define Person with an optional job_title.

pub struct Person {
    pub name: String,
    pub age: i32,
    pub hobby: String,
    pub job_title: Option<String>,
    pub favorite_color: Color,
}

Now, we can write logic that depends on the state of the job_title.

let me: Person = Person {
    name: "Matthew Pagan".to_string(),
    age: 32,
    hobby: "Coding".to_string(),
    job_title: Some("Software Developer".to_string()),
    favorite_color: Color::Cyan,
};

let noah: Person = Person {
    name: "Noah Pagan".to_string(),
    age: 5,
    hobby: "Lego".to_string(),
    job_title: None,
    favorite_color: Color::Blue,
};

match me.job_title {
    Some(title) => println!("{} is a {}", me.name, title),
    None => println!("{} doesn't have a job", me.name),
}

match noah.job_title {
    Some(title) => println!("{} is a {}", noah.name, title),
    None => println!("{} doesn't have a job", noah.name),
}
Option Enum

Option<T> enums can either be 1 of 2 variants

  • Some<T>: Represent the presence of a value, of type T
  • None: Represents the lack of a value

Using match to perform actions, depending on the value of an enum, is one of the most common patterns I've experienced diving into the world of Rust, and provides an effective way for handling control flow with enums.

Functions

Like other languages, we can encapsulate repeated logic in functions. We're repeating our logic when we're handling what to print depending on the value of job_title. Let's create a function that takes in a Person, and logs to standard out the correct message.

fn handle_job_title(person: Person) -> () {
    match person.job_title {
        Some(title) => println!("{} is a {}", person.name, title),
        None => println!("{} doesn't have a job", person.name),
    }
}

let me: Person = Person {
    name: "Matthew Pagan".to_string(),
    age: 32,
    hobby: "Coding".to_string(),
    job_title: Some("Software Developer".to_string()),
    favorite_color: Color::Cyan,
};

let noah: Person = Person {
    name: "Noah Pagan".to_string(),
    age: 5,
    hobby: "Lego".to_string(),
    job_title: None,
    favorite_color: Color::Blue,
};

handle_job_title(me);
handle_job_title(noah);

I even get some nice IntelliSense for everything so far using VSCode if I import this function from a module.

Function Intellisense

Arrays and Ownership

We can clean up this code even more. We are declaring and initializing 2 Person objects. We could store this in an Array, and then iterate through that Array, running the function on each iteration.

Here is an example of how to implement this using Rust.

let people: [Person; 2] = [
    Person {
        name: "Matthew Pagan".to_string(),
        age: 32,
        hobby: "Coding".to_string(),
        job_title: Some("Software Developer".to_string()),
        favorite_color: Color::Cyan,
    },
    Person {
        name: "Noah Pagan".to_string(),
        age: 5,
        hobby: "Lego".to_string(),
        job_title: None,
        favorite_color: Color::Blue,
    },
];

for person in people.iter() {
    handle_job_title(person);
}
Array Implementation

To get this working though, we need to make some minor changes to our function implementation, and instruct the function to borrow the Person argument by adding a & in front of the argument type.

use super::super::types::Person;

pub fn handle_job_title(person: &Person) -> () {
    match &person.job_title {
        Some(title) => println!("{} is a {}", person.name, title),
        None => println!("{} doesn't have a job", person.name),
    }
}

This broaches a core Rust feature, the subject of Ownership. Ownership has 3 basic rules:

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

Let's create a pair of examples to show the ideas of Ownership.

fn add_numbers(x: i32, y: i32) -> i32 {
    x + y
}

let number = 3;

println!("{} + {} = {}", 1, x, add_numbers(1, number));
println!("{} + {} = {}", 1, x, add_numbers(5, number));

This works just as we would expect it to work, printing out to console the results of both function calls. Because integers are simple values with a known, fixed size, this works by pushing a copy of the i32 integer onto the stack for the value of the function argument.

Because we're making copies of the integer number onto the stack, we adhere to the rules of Ownership and number is the sole owner of its copy of the integer 3.

Let's try this with a function that takes a String.

fn print_string(x: String) {
    println!("{}", x);
}

let name: String = String::from("Matthew Pagan");

print_string(name);
print_string(name);

This does NOT work. VS Code throws an error right away when we try this on the second call of the print_string function.

Ownership Error

The error says that we used a moved value. If we look at the third rule of Ownership, it states, "When the owner goes out of scope, the value will be dropped."

Unlike integers, the String type is a growable, mutable, owned, UTF-8 encoded string type. Unlike the scalar integer, a `String' is pushed onto the heap, a much more expensive place to keep memory.

Because of this expense, the Rust compiler doesn't just copy the value into the function argument. Instead, the function arg becomes the new owner of that data.

To repeat, when we called print_string, we left the scope where we declared the name String, and the function argument x became the new owner of the value "Matthew Pagan". After the function logged my name, and we left its scope, name no longer was the owner of the data, throwing an error when we tried to use it again.

To fix this, we need to tell the function to only borrow the value of the String, and to return ownership of that value to the name variable once we leave the function scope. We do that by pre-pending a & in front of the function argument type. Below is the implementation to fix our move error.

fn print_string(x: &String) {
    println!("{}", x);
}

let name = String::from("Matthew Pagan");
print_string(&name);
print_string(&name);

Conclusion

Rust seems like it took the best parts of the JavaScript ecosystem, and supercharged them into a feature-packed programming language, purpose-built to get developers into systems programming. I will heavily admit that it has certainly worked with me and has drawn an unhealthy amount of my attention.

There remains so much more left to do with our growing knowledge of Rust. In my next blog post, we'll explore how to build on this foundation of knowledge, and start working with external Rust packages (crates) to create a webserver to handle the back-end role of a full-stack application.

Until then, install if you haven't done so yet, install Rust and start playing around with it today! It's seriously the easiest way to get into lower-level programming, especially if you're from the world of web development, like me.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

How to Create a Custom React Renderer cover image

How to Create a Custom React Renderer

Creating a Custom React Renderer At the very top of the React documentation, the team defines React's main qualities: - Declarative - Component-Based - Learn Once, Write Anywhere The main focus of the React docs is to demonstrate the first 2 qualities of React: its declarative nature, and how it allows the developer to break logic down into components. The main goal of this article will be to expand upon that third quality of React: "Learn Once, Write Anywhere." Requirements To follow along more easily with this post, you should already know a few things: - React: This post doesn't teach you how to declaratively write React, but instead dives into how React communicates with the DOM. Understanding how to write generic React code would be great foundational knowledge before diving into how it works under the hood. - The DOM: A lot of the interactions between React and the DOM are abstracted away under the 'react-dom' package. Having a good understanding of how to render to the DOM with vanilla JavaScript will be incredibly useful since we will be implementing this functionality ourselves. I've also created a repository based on the create react app starter, and added some nice-to-have features to it including: - TypeScript - ESLint - Prettier - Husky w/ pre-commit linting - Tailwind CSS You can test out your code using a vanilla create-react-app installation, but I enjoy these developer tools, so I wanted to offer a configured setup that uses them. Feel free to clone and use the repo! React DOM I'm sure that every single React developer has run create-react-app at least once. When creating a CRA app, 99% of your time is usually spent expanding the functionality of the App component. Very little time is spent on the piece of logic that actually renders the App component. This line is responsible for taking our React App component, and then mounting all of its components along with event handlers, to the DOM. We usually never need to worry about how React does this. Instead, we focus on declaratively adding functionality to the App components. Rendering to the DOM is abstracted away into this one line. This is similar to working with React Native. We develop Native App Components, but we don't think about *how* those components are rendered to different devices. React Native handles that for us, just like how ReactDOM handles rendering to the DOM for us. The Test Application To test out experimenting with our own custom React renderer, I've created a repository forked off of a create-react-app install, with added dev features like linting, git commit hooks, and TailwindCSS. Before diving into replacing ReactDOM, let's look at what our application can look like at the start. Replacing ReactDOM To replace the react-dom renderer with our own, we'll need to import 2 dependencies: - react-reconciler: This exposes a function that takes a host configuration object, allowing us to customize rendering to whatever format we desire. - @types/react-reconciler: Types for react-reconciler After installing these dependencies, we can replace ReactDOM with our new renderer, and then with the help of TypeScript, stub out the remaining portions of our new Renderer. What does a React renderer look like? The React team exposes their react-reconciler as a function to allow third parties to create custom renderers. This reconciler function takes one argument: a Host Configuration object that's methods provide an interface with which React can render to a host environment. The methods of the host configuration object map out to different methods of the configured host environment, allowing the developer to abstract away the process of rendering and updating the state to the environment. ` For example, here is real code from react-dom, which defines how to append a child in the DOM. In our example exploring this, we'll try and minimally recreate react-dom, so we can render our sample app to the DOM. Host Configuration With a stubbed DOM host configuration, and having replaced ReactDOM with our custom renderer, we are now able to run the CRA dev server without errors. However, nothing has been rendered to the DOM yet. Our host configuration method stubs included console.log's, showing when these methods get called though, so the log has a lot of activity. We can see bits and pieces of our App component in these logs, but since our host configuration did not actually mount anything to the DOM, our screen remains blank. Let's fill out a few of our functions to implement this behavior: - createInstance - createTextInstance - appendInitialChild - appendChild - appendChildToContainer TypeScript does a lot of mental heavy lifting by allowing us to define types and enhancing our development with auto-complete for implementing these host config functions. Types and generic host config signature: ` The createInstance function: ` The createTextInstance function: ` The appendChild function: ` In the end, it was just a few familiar DOM calls until we were able to render our application once again, except this time, with our own renderer! Conclusion With just a few method definitions, we are now able to render to the DOM, but we could have also just as easily issued commands to draw on a canvas when trying to render our components, or we could have rendered differently. By learning React once, you can apply it in a number of scenarios. By separating rendering logic from reconciliation logic, React allows third-party developers to create custom renderers. This allows developers to render whereever they want, be it in the canvas, or even to the console....

The simplicity of deploying an MCP server on Vercel cover image

The simplicity of deploying an MCP server on Vercel

The current Model Context Protocol (MCP) spec is shifting developers toward lightweight, stateless servers that serve as tool providers for LLM agents. These MCP servers communicate over HTTP, with OAuth handled clientside. Vercel’s infrastructure makes it easy to iterate quickly and ship agentic AI tools without overhead. Example of Lightweight MCP Server Design At This Dot Labs, we built an MCP server that leverages the DocuSign Navigator API. The tools, like `get_agreements`, make a request to the DocuSign API to fetch data and then respond in an LLM-friendly way. ` Before the MCP can request anything, it needs to guide the client on how to kick off OAuth. This involves providing some MCP spec metadata API endpoints that include necessary information about where to obtain authorization tokens and what resources it can access. By understanding these details, the client can seamlessly initiate the OAuth process, ensuring secure and efficient data access. The Oauth flow begins when the user's LLM client makes a request without a valid auth token. In this case they’ll get a 401 response from our server with a WWW-Authenticate header, and then the client will leverage the metadata we exposed to discover the authorization server. Next, the OAuth flow kicks off directly with Docusign as directed by the metadata. Once the client has the token, it passes it in the Authorization header for tool requests to the API. ` This minimal set of API routes enables me to fetch Docusign Navigator data using natural language in my agent chat interface. Deployment Options I deployed this MCP server two different ways: as a Fastify backend and then by Vercel functions. Seeing how simple my Fastify MCP server was, and not really having a plan for deployment yet, I was eager to rewrite it for Vercel. The case for Vercel: * My own familiarity with Next.js API deployment * Fit for architecture * The extremely simple deployment process * Deploy previews (the eternal Vercel customer conversion feature, IMO) Previews of unfamiliar territory Did you know that the MCP spec doesn’t “just work” for use as ChatGPT tooling? Neither did I, and I had to experiment to prove out requirements that I was unfamiliar with. Part of moving fast for me was just deploying Vercel previews right out of the CLI so I could test my API as a Connector in ChatGPT. This was a great workflow for me, and invaluable for the team in code review. Stuff I’m Not Worried About Vercel’s mcp-handler package made setup effortless by abstracting away some of the complexity of implementing the MCP server. It gives you a drop-in way to define tools, setup https-streaming, and handle Oauth. By building on Vercel’s ecosystem, I can focus entirely on shipping my product without worrying about deployment, scaling, or server management. Everything just works. ` A Brief Case for MCP on Next.js Building an API without Next.js on Vercel is straightforward. Though, I’d be happy deploying this as a Next.js app, with the frontend features serving as the documentation, or the tools being a part of your website's agentic capabilities. Overall, this lowers the barrier to building any MCP you want for yourself, and I think that’s cool. Conclusion I'll avoid quoting Vercel documentation in this post. AI tooling is a critical component of this natural language UI, and we just want to ship. I declare Vercel is excellent for stateless MCP servers served over http....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co