“Rust and TypeScript”



This content originally appeared on DEV Community and was authored by GAUTAM MANAK

Rust and TypeScript: A Powerful Duo for Modern Development

Rust and TypeScript, two seemingly disparate languages, are increasingly finding common ground in modern software development. Rust, known for its unparalleled performance and memory safety, excels in system-level programming and performance-critical applications. TypeScript, a superset of JavaScript, brings strong typing and improved tooling to web development, making large-scale JavaScript projects more manageable. This post explores how these two languages can complement each other, creating robust and efficient applications from backend to frontend. We’ll dive into practical examples, demonstrating how Rust can power your backend while TypeScript handles the complexities of your frontend. By understanding their strengths and weaknesses, you can leverage the best of both worlds for a superior development experience.

Why Combine Rust and TypeScript?

The combination of Rust and TypeScript offers several compelling advantages. Rust provides a solid foundation for backend services, offering:

  • Performance: Rust’s zero-cost abstractions and low-level control allow for highly optimized code, crucial for demanding applications.
  • Memory Safety: Rust’s borrow checker eliminates common memory errors like dangling pointers and data races, leading to more reliable and secure applications.
  • Concurrency: Rust’s ownership system makes concurrent programming safer and easier to reason about.

TypeScript, on the other hand, shines in frontend development with features like:

  • Strong Typing: TypeScript’s static typing helps catch errors early in the development process, improving code quality and maintainability.
  • Improved Tooling: TypeScript benefits from excellent IDE support, code completion, and refactoring capabilities.
  • Large Ecosystem: TypeScript leverages the vast JavaScript ecosystem, providing access to a wide range of libraries and frameworks.

By using Rust for performance-sensitive tasks and TypeScript for the user interface, you can build applications that are both fast and maintainable.

Building a Simple Rust Backend with actix-web

Let’s create a basic Rust backend using the actix-web framework, a popular choice for building web applications. We’ll create a simple API endpoint that returns a JSON response.

First, you’ll need to have Rust installed. Then, create a new Rust project:

cargo new rust-typescript-example
cd rust-typescript-example

Next, add the actix-web and serde dependencies to your Cargo.toml file:

[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Now, let’s create the main Rust file (src/main.rs):

use actix_web::{get, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Message {
    text: String,
}

#[get("/hello")]
async fn hello() -> impl Responder {
    let message = Message {
        text: "Hello from Rust!".to_string(),
    };
    HttpResponse::Ok().json(message)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Explanation:

  • We import the necessary modules from actix-web and serde.
  • We define a Message struct with a text field, using serde to enable serialization and deserialization to JSON.
  • The hello function is an asynchronous handler that returns a JSON response containing a greeting.
  • The #[get("/hello")] attribute maps the /hello route to the hello function.
  • The main function sets up the actix-web server, registers the hello service, and starts the server on 127.0.0.1:8080.

To run the backend, execute:

cargo run

You can now access the API endpoint at http://127.0.0.1:8080/hello and see the JSON response.

Building a TypeScript Frontend with fetch

Now, let’s create a simple TypeScript frontend that consumes the Rust backend API. We’ll use the fetch API to make the request.

First, you’ll need Node.js and npm installed. Create a new TypeScript project:

mkdir typescript-frontend
cd typescript-frontend
npm init -y
npm install typescript --save-dev
npx tsc --init

Next, install the node-fetch package to use fetch in a Node.js environment. This is useful for running the code locally and testing before deploying to a browser environment.

npm install node-fetch --save
npm install @types/node-fetch --save-dev

Now, create a TypeScript file (src/index.ts):

import fetch, { Response } from 'node-fetch';

interface Message {
  text: string;
}

async function fetchData(): Promise<void> {
  try {
    const response: Response = await fetch('http://127.0.0.1:8080/hello');

    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }

    const data: Message = await response.json() as Message;
    console.log('Data from Rust backend:', data.text);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

fetchData();

Explanation:

  • We import the fetch function from node-fetch.
  • We define a Message interface that matches the structure of the JSON response from the Rust backend.
  • The fetchData function makes a GET request to the /hello endpoint of the Rust backend.
  • We check if the response is successful (status code 200-299).
  • We parse the response body as JSON and cast it to the Message interface.
  • We log the text field of the message to the console.
  • We handle potential errors during the fetch operation.

To compile and run the TypeScript code, execute:

npx tsc
node dist/index.js

You should see the message “Data from Rust backend: Hello from Rust!” printed to the console.

Data Validation and Type Safety

One of the benefits of using TypeScript on the frontend is type safety. You can define interfaces that match the structure of the data returned by the Rust backend, ensuring that your frontend code handles the data correctly. While the example above directly casts the result of response.json() to the Message interface, for production applications it’s often beneficial to use a validation library like zod or io-ts to ensure the data received from the backend conforms to the expected schema. This can help prevent runtime errors caused by unexpected data formats.

For example, using zod:

import fetch, { Response } from 'node-fetch';
import { z } from 'zod';

const MessageSchema = z.object({
  text: z.string(),
});

type Message = z.infer<typeof MessageSchema>;

async function fetchData(): Promise<void> {
  try {
    const response: Response = await fetch('http://127.0.0.1:8080/hello');

    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }

    const rawData = await response.json();
    const data: Message = MessageSchema.parse(rawData); // Validate the data
    console.log('Data from Rust backend:', data.text);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

fetchData();

In this example, zod is used to define a schema for the Message type. The MessageSchema.parse(rawData) line validates that the data received from the backend conforms to the defined schema. If the data does not conform, zod will throw an error, preventing the application from proceeding with invalid data. This approach improves the robustness and reliability of your frontend application.

Building More Complex Systems

This simple example demonstrates the basic principles of combining Rust and TypeScript. In more complex systems, you might use Rust to implement computationally intensive tasks, such as image processing, data analysis, or game engine logic. These tasks can be exposed as APIs that are consumed by a TypeScript frontend. You could also use a framework like Tauri to build cross-platform desktop applications using Rust for the backend and TypeScript for the frontend. The possibilities are vast, and the combination of Rust and TypeScript provides a powerful foundation for building modern, high-performance applications.

Conclusion

Rust and TypeScript offer a powerful combination for modern software development. Rust’s performance and safety make it ideal for backend services, while TypeScript’s strong typing and tooling improve the maintainability of frontend code. By leveraging the strengths of both languages, you can build applications that are both fast and reliable. As a next step, explore frameworks like Tauri, learn more about data validation libraries, and experiment with building more complex applications that leverage the full potential of Rust and TypeScript.


This content originally appeared on DEV Community and was authored by GAUTAM MANAK