npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

typespec-rust-emitter

v0.9.0

Published

TypeSpec emitter that generates idiomatic Rust types and structs

Readme

Authored by opencode & Qwen


TypeSpec Rust Emitter

A TypeSpec emitter that generates idiomatic Rust types and structs from TypeSpec specifications.

npm version License: MIT

Features

  • Models: Converts TypeSpec models to Rust structs with serde derive macros
  • Enums: Supports both string and integer enums with Default derive
  • Unions: Handles nullable types (T | nullOption<T>) and string literal unions
  • Scalars: Maps TypeSpec scalars to Rust equivalents with @format support
  • Inheritance: Supports model inheritance with getAllProperties()
  • Error Models: Generates thiserror::Error derive with #[error(...)] attributes and IntoResponse impl for axum
  • Pattern Validation: Supports @pattern decorators with TryFrom<String> validation
  • Custom Derives: Add arbitrary Rust derive macros via @rustDerive decorator (models & enums)
  • Custom Attributes: Add arbitrary Rust attributes via @rustAttr decorator (models & enums)
  • Server Trait: Generates axum server trait and router from HTTP operations

Installation

npm install typespec-rust-emitter

Peer Dependencies

This package requires the following peer dependencies:

npm install @typespec/compiler @typespec/emitter-framework

Usage

Basic Model

import "typespec-emitter";

model User {
  name: string;
  age: int32;
  email: string | null;
}

Generates:

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct User {
    #[serde(rename = "name")]
    pub name: String,
    #[serde(rename = "age")]
    pub age: i32,
    #[serde(rename = "email")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub email: Option<String>,
}

String Enum

enum Status {
  active,
  inactive,
  pending,
}

Generates:

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
pub enum Status {
    #[default]
    #[serde(rename = "active")]
    Active,
    #[serde(rename = "inactive")]
    Inactive,
    #[serde(rename = "pending")]
    Pending,
}

Error Model

@error
model ApiError {
  code: string;
  message: string;
}

Generates:

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, thiserror::Error)]
#[error("{code}: {message}")]
pub struct ApiError {
    #[serde(rename = "code")]
    pub code: String,
    #[serde(rename = "message")]
    pub message: String,
}

impl IntoResponse for ApiError {
    fn into_response(self) -> axum::response::Response {
        (
            StatusCode::from_str(&self.code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
            Json(self),
        )
            .into_response()
    }
}

Error codes are parsed dynamically, so any valid HTTP status code string (e.g., "NOT_FOUND", "BAD_REQUEST", "418 I'M_A_TEAPOT") will work.

UUID Format

@format("uuid")
scalar Uuid extends string;

Generates:

pub type Uuid = uuid::Uuid;

Pattern Validation

@pattern("^#[0-9A-Fa-f]{6}$")
scalar HexColor extends string;

Generates:

#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct HexColor(pub String);

impl TryFrom<String> for HexColor {
    type Error = String;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        let re = regex::Regex::new(r"^#[0-9A-Fa-f]{6}$").unwrap();
        if re.is_match(&value) { Ok(Self(value)) } else { Err(format!("Invalid value: {}", value)) }
    }
}

Custom Derives

import "typespec-emitter";

@rustDerive("sqlx::FromRow")
@rustDerive("derive_more::Display")
model GroupStatistics {
  id: int64;
  name: string;
}

Generates:

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow, derive_more::Display)]
pub struct GroupStatistics {
    #[serde(rename = "id")]
    pub id: i64,
    #[serde(rename = "name")]
    pub name: String,
}

Custom Attributes

import "typespec-emitter";

@rustAttr("sqlx(type_name = \"user\")")
model User {
  name: string;
}

@rustDerive("sqlx::Type")
@rustAttr("sqlx(type_name = \"study_status\")")
enum StudyStatus {
  Starting,
  Paused,
}

Generates:

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[sqlx(type_name = "user")]
pub struct User {
    #[serde(rename = "name")]
    pub name: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize, sqlx::Type)]
#[sqlx(type_name = "study_status")]
pub enum StudyStatus {
    #[default]
    #[serde(rename = "Starting")]
    Starting,
    #[serde(rename = "Paused")]
    Paused,
}

Server Trait Generation

The emitter can generate a complete axum server trait and router from TypeSpec HTTP operations.

Example TypeSpec

@route("/groups")
namespace Groups {
  @get
  @route("")
  op list(): Group[];

  @post
  @route("")
  @useAuth(BearerAuth)
  op create(@body body: CreateGroupBody): Group;

  @get
  @route("/{id}")
  op getById(@path id: int64): Group;
}

Generated Server Trait

#[async_trait]
pub trait Server: Send + Sync {
    type Claims: Send + Sync + 'static;

    async fn groups_list(&self) -> Result<GroupsListResponse>;
    async fn groups_create(&self, claims: Self::Claims, body: CreateGroupBody) -> Result<GroupsCreateResponse>;
    async fn groups_get_by_id(&self, id: i64) -> Result<GroupsGetByIdResponse>;
}

Implementing the Server

#[derive(Clone)]
pub struct AppState {
    pub db: Arc<SqlitePool>,
}

pub struct MyServer {
    state: AppState,
}

#[async_trait::async_trait]
impl Server for MyServer {
    type Claims = Claims; // Your custom claims type

    async fn groups_list(&self) -> eyre::Result<GroupsListResponse> {
        let groups = sqlx::query_as::<_, Group>("SELECT * FROM groups")
            .fetch_all(&self.state.db)
            .await?;
        Ok(GroupsListResponse::Ok(Json(groups)))
    }

    async fn groups_create(
        &self,
        claims: Self::Claims,
        body: CreateGroupBody,
    ) -> eyre::Result<GroupsCreateResponse> {
        // Direct access to body fields
        let group = sqlx::query_as::<_, Group>(
            "INSERT INTO groups (name, owner_id) VALUES ($1, $2) RETURNING *"
        )
        .bind(&body.name)
        .bind(&claims.sub)
        .fetch_one(&self.state.db)
        .await?;
        Ok(GroupsCreateResponse::Created(Json(group)))
    }

    async fn groups_get_by_id(&self, id: i64) -> eyre::Result<GroupsGetByIdResponse> {
        // Direct access to path parameter
        let group = sqlx::query_as::<_, Group>("SELECT * FROM groups WHERE id = $1")
            .bind(id)
            .fetch_optional(&self.state.db)
            .await?
            .ok_or_else(|| GroupsGetByIdResponse::NotFound)?;
        Ok(GroupsGetByIdResponse::Ok(Json(group)))
    }

    // ... implement other methods
}

Creating the Router

// Define your claims type
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Claims {
    pub sub: String,
    pub exp: usize,
}

// Create auth middleware
async fn auth_middleware(
    Extension(claims): Extension<Claims>,
    request: Request<axum::body::Body>,
    next: Next,
) -> Result<Response, StatusCode> {
    // Validate claims (e.g., check expiration)
    if claims.exp < (chrono::Utc::now().timestamp() as usize) {
        return Err(StatusCode::UNAUTHORIZED);
    }
    Ok(next.run(request).await)
}

// Apply middleware to protected routes
fn with_auth<S>(router: Router<S>) -> Router<S>
where
    S: Server + Clone + Send + Sync + 'static,
    S::Claims: Send + Sync + Clone + 'static,
{
    router.layer(axum::middleware::from_fn(auth_middleware))
}

// Create the router
let server = MyServer::new(state);
let router = create_router(server, with_auth);
axum::serve(listener, router).await?;

Protected Routes

Operations decorated with @useAuth will:

  • Receive claims: Self::Claims as the first parameter
  • Be wrapped with the middleware passed to create_router

Public routes do not receive claims and are not wrapped with auth middleware.

Type Mappings

| TypeSpec Type | Rust Type | | ---------------- | ------------------------------------------- | | string | String | | int8/16/32/64 | i8/i16/i32/i64 | | uint8/16/32/64 | u8/u16/u32/u64 | | float32/64 | f32/f64 | | boolean | bool | | bytes | Vec<u8> | | string[] | Vec<String> | | Record<string> | std::collections::HashMap<String, String> | | T \| null | Option<T> | | utcDateTime | chrono::DateTime<chrono::Utc> | | offsetDateTime | chrono::DateTime<chrono::FixedOffset> | | plainDateTime | chrono::NaiveDateTime | | plainDate | chrono::NaiveDate | | plainTime | chrono::NaiveTime |

Format Mappings

| @format() | Rust Type | | ------------- | ----------------------- | | "uuid" | uuid::Uuid | | "date" | chrono::NaiveDate | | "time" | chrono::NaiveTime | | "date-time" | chrono::DateTime<Utc> |

Decorators

| Decorator | Description | | ---------------------------- | ----------------------------------------------------- | | @error | Adds thiserror::Error derive with error message | | @pattern("regex") | Generates TryFrom<String> with regex validation | | @rustDerive("...") | Adds a custom derive macro (models & enums) | | @rustDerives("...", "...") | Adds multiple custom derive macros (models & enums) | | @rustAttr("...") | Adds a custom Rust attribute (models & enums) | | @rustAttrs("...", "...") | Adds multiple custom Rust attributes (models & enums) | | @doc("...") | Generates /// doc comments | | @useAuth(AuthType) | Marks operation as protected, adds Claims parameter |

Building

npm run build

Testing

npm test

Development

The emitter is built using the TypeSpec emitter framework. Key files:

  • src/emitter.ts - Main emitter implementation
  • src/lib.tsp - TypeSpec decorator declarations
  • src/index.ts - Public exports
  • test/hello.test.ts - Unit tests
  • example/ - Example TypeSpec definitions and generated Rust output

License

MIT