Auto-generating API service using Rust, to TypeScript & Dart

No body got time for errors

Olly Dixon - CTO of Polydelic
Olly Dixon - CTO @ Polydelic
Monday 15th of Jan

Prerequisites

  • Knowledge of full stack development
  • Rust installed
  • NodeJS and NPM setup
  • Dart/Flutter setup
  • Mac/Linux or are using Windows WSL

The Problem

When working in teams across APIs it's important that objects, types and classes are accurate between the client and the services.

The amount of bugs I've seen from Rest API calls with improper types, missing properties and much more is astonishing.

We looked into several technologies, GraphQL, tRPC, Rust Type Convert, gRPC, and others to determine which suited our needs. Unfortunately they were missing some important feature for automatic client conversion, such as missing error types, missing return types, everything being optional (GraphQL..)

After trying many different APIs, we stumbled upon Salvo, for Rust.

With Salvo you can automatically generate an entire Swagger schema automatically, with the returns types, error enums and more much included.

You don't have to do any manual work, like manually specify the OpenAPI properties. It will convert structs to specs.

In this tutorial; I will make a basic autogen project. This project generates the API services from the Rust backend to TypeScript and Dart clients. You can find a full version of the project Github link to demo project. In the end you will have a simple script to fully generate API services from your Rust backend.

Let's begin

First, make a project directory inside the terminal

mkdir ./my-auto-gen-project
cd my-auto-gen-project

The Rust Project with Salvo

Create our Rust project

				cargo new my_api
cd my_api
// Open in VSC code (optional)
code .

			

Add the following to Cargo.toml

[dependencies]
salvo = { version = "*", features = ["oapi", "cors"] }
tokio = { version = "1", features = ["macros"] }
tracing = "0.1.40"
tracing-subscriber = "0.3"

Open src/main.rs and add the following, this will setup our API and our UI

				
use salvo::cors::Cors;
use salvo::hyper::Method;
use salvo::oapi::extract::*;
use salvo::prelude::*;

#[endpoint]
async fn hello(name: QueryParam<String, false>) -> String {
    format!("Hello, {}!", name.as_deref().unwrap_or("World"))
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt().init();

    let router = Router::new().push(Router::with_path("hello").get(hello));

    let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);

    // Allow requests from origin
    let cors = Cors::new()
        .allow_origin("*")
        .allow_methods(vec![Method::GET, Method::POST, Method::DELETE])
        .into_handler();

    let router = router
        .push(doc.into_router("/api-doc/openapi.json"))
        .push(SwaggerUi::new("/api-doc/openapi.json").into_router("ui"))
        .hoop(cors);

    let acceptor = TcpListener::new("127.0.0.1:5800").bind().await;
    Server::new(acceptor).serve(router).await;
}

			

Now open the terminal inside your editor and write

cargo run

You should be able to visit http://127.0.0.1:5800/ui/

You'll be presented with the Swagger UI

swagger ui

Our endpoints are automatically generated thanks to Salvo

swagger ui

As you can see our API, documentation and playground is fully generated from this small bit of code, which is amazing.

Now if we tap the link under the title on this page /api-doc/openapi.json we will get the Swagger spec.

It looks like this

swagger ui

We will use this as a basis for our TypeScript and Dart (Flutter) projects to generate our APIs automatically.

The Typescript project

Now that we have out basic API setup, and swagger docs working, let’s generate the frontend clients, starting with TypeScript.

For a dummy framework we're going to use SvelteKit, since we feel it’s the shortest amount of work. It really doesn't matter which framework you use, React, Vue, etc, they all will work as we are generating an API service that can be used on any TypeScript project (SSR might have some modifications)

Generate the Svelte-kit project

// Svelte project
npm create svelte@latest web-frontend
(select SvelteKit demo app, Typescript, ESLint)

cd web-frontend
npm install

// Optional, we're opening in our editor VSC
code .

// Check that the project runs
npm run dev

// Open in the browser
http://localhost:5173/

You should see a SvelteKit example project.

sveltekit example ui

Now, time to generate that juicy API!

Open package.json inside the Svelte Project.

We will add the following “script” line, and call it “gen-api”

// You can define different types, of requesters, for example typescript-[client|axios|fetch]
npx @openapitools/openapi-generator-cli generate -i http://127.0.0.1:5800/api-doc/openapi.json -g typescript-fetch -o ./src/api/

There are additional options, like the base path, etc. It should look like this:

"scripts": {
		"dev": "vite dev",
		"build": "vite build",
	    "gen-api": "npx @openapitools/openapi-generator-cli generate -i http://127.0.0.1:5800/api-doc/openapi.json -g typescript-fetch -o ./src/api/",
		"preview": "vite preview",
		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
	},

Now if we want to generate our API we just run

npm run gen-api 

You should see the api pop up under SRC

swagger ui

Let's add the API to a page, and test it.

Go to src/routes/+page.svelte

Change the page code to look like this

<script>
import { Configuration, DefaultApi } from "../api";
import { onMount } from "svelte";

let test = "this is a test";

onMount(async () => {
	console.log("onMount");

	const config = new Configuration({
	basePath: "http://localhost:5800",
	});

	const api = new DefaultApi(config);

	test = await api.myApiHello({ name: "Olly" });
});
</script>

<svelte:head>
<title>Home</title>
<meta name="description" content="Svelte demo app" />
</svelte:head>

<section>
<h1>
	{test}
</h1>
</section>

<style>
section {
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
	flex: 0.6;
}

h1 {
	width: 100%;
}
</style>
			

You should see (given that API is running and frontend is running)

swagger ui

Here we are using the auto-generated TypeScript API. It even comes with auto-complete, parameters, error types and input/output.

This really is a great to any frontend developers looking to fully automate their API services.

swagger ui

I know now that my API is 100% accurate, no more writing manual services for teams, easy updates and much more.

Now the Flutter app and Dart code

This will also work with Dart backend for your external services.

We tried a few dart auto-gen tools, unfortunately many of them were broken or missed key parts of the API out (like return types!)

Finally we used one called “swagger_dart_code_generator”

It seems to fully work, meaning it generates types, enums, complex objects and much more. The official swagger cli was actually broken with complex objects, so we had to use this

Go to the root of our project again and generate the flutter app

flutter create mobile_app
cd mobile_app
// Optional opening the project
code .

Open the pubspec.yaml file

Make sure it looks like this

name: mobile_app
description: "My auto-gen project"

# pub.dev using 'flutter pub publish'. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 1.0.0+1

environment:
	sdk: '>=3.2.3 <4.0.0'

dependencies:
	flutter:
	sdk: flutter
	chopper: ^7.0.10
	json_annotation: ^4.8.0
	swagger_dart_code_generator: ^2.14.2

	cupertino_icons: ^1.0.2

dev_dependencies:
	flutter_test:
	sdk: flutter
	build_runner: ^2.3.3
	chopper_generator: ^7.0.7
	json_serializable: ^6.6.1
	flutter_lints: ^3.0.1

flutter:
	uses-material-design: true

Create a build.yaml in the root of the mobile project, make sure it looks like below

targets:
$default:
	sources:
	- lib/**
	- open_api/**
	builders:
	swagger_dart_code_generator:
		# https://pub.dev/packages/swagger_dart_code_generator
		options:
		input_folder: "open_api/"
		output_folder: "lib/generated_api/"
		add_base_path_to_requests: true
		input_urls: 
			- url: "http://127.0.0.1:5800/api-doc/openapi.json"

What this does it connect to the swagger spec, and generate the dart code.

Make a “open_api” folder in the root of the app project

Now run this with in the terminal

dart run build_runner build

Two things should have happened

1. The openapi.json spec should have been downloaded

2. The generated API should have been created from it.

swagger ui

Now that this is setup, you can use the API just like the TypeScript project.

My go to main.dart

We are going to modify the default counter app. When you press the + button it will make a request and fill the text in.

import 'package:flutter/material.dart';
import 'package:mobile_app/generated_api/client_index.dart';

void main() {
	runApp(const MyApp());
}

class MyApp extends StatelessWidget {
	const MyApp({super.key});

	@override
	Widget build(BuildContext context) {
	return MaterialApp(
		title: 'Flutter Demo',
		theme: ThemeData(
		colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
		useMaterial3: true,
		),
		home: const MyHomePage(title: 'Flutter Demo Home Page'),
	);
	}
}

class MyHomePage extends StatefulWidget {
	const MyHomePage({super.key, required this.title});

	final String title;

	@override
	State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
	String _text = "";

	void _incrementCounter() {
	_test();
	}

	@override
	void initState() {
	super.initState();
	}

	_test() async {
	print("test");

	final Openapi api = Openapi.create(
		baseUrl: Uri.parse("http://localhost:5800"),
	);

	try {
		final data1 = await api.helloGet(name: "Olly");
		setState(() {
		_text = data1.body!; // << Is string, or can be complex object.
		});
	} catch (e) {
		print(e);
	}
	}

	@override
	Widget build(BuildContext context) {
	return Scaffold(
		appBar: AppBar(
		backgroundColor: Theme.of(context).colorScheme.inversePrimary,
		title: Text(widget.title),
		),
		body: Center(
		child: Column(
			mainAxisAlignment: MainAxisAlignment.center,
			children: <Widget>[
			Text(_text),
			],
		),
		),
		floatingActionButton: FloatingActionButton(
		onPressed: _incrementCounter,
		tooltip: 'Increment',
		child: const Icon(Icons.add),
		),
	);
	}
}
swagger ui

When you tap the plus button, it goes to the API, and back. You can even use complex types here. The Rust API will also support complex type returns, validation and much more. Our full version also supports error enums, and status code strings so the developer knows exactly what to handle.

Putting it all together

I have a full version of the project here, open to anyone: Github link to demo project

If you would like help setting this up, we're open to work at Polydelic AS, use the contact form below.

Map of Oslo

Talk with us