Tutorial

This tutorial will teach you TypeMUX from the ground up, building a complete e-commerce API schema.

Table of Contents

  1. Basic Types
  2. Enums
  3. Complex Types
  4. Arrays and Maps
  5. Services and Methods
  6. Annotations and Attributes
  7. Union Types
  8. Namespaces
  9. Multiple Files and Imports
  10. External YAML Annotations

Basic Types

TypeMUX supports these primitive types:

Let’s create a simple product type:

type Product {
  id: string
  name: string
  price: float64
  inStock: bool
}

Enums

Enums define a set of named constants:

enum ProductCategory {
  ELECTRONICS
  CLOTHING
  BOOKS
  FOOD
  OTHER
}

Add the category to the Product:

type Product {
  id: string
  name: string
  price: float64
  inStock: bool
  category: ProductCategory
}

Complex Types

Types can reference other types. Let’s add a supplier:

type Supplier {
  id: string
  name: string
  email: string
  phone: string
}

type Product {
  id: string
  name: string
  price: float64
  inStock: bool
  category: ProductCategory
  supplier: Supplier
}

Arrays and Maps

Arrays

Use [] to define arrays:

type Product {
  id: string
  name: string
  price: float64
  inStock: bool
  category: ProductCategory
  supplier: Supplier
  tags: []string
  images: []string
}

Maps

Use map<KeyType, ValueType> for key-value pairs:

type Product {
  id: string
  name: string
  price: float64
  inStock: bool
  category: ProductCategory
  supplier: Supplier
  tags: []string
  images: []string
  attributes: map<string, string>
}

Maps are converted differently by each output format:

Nested maps are fully supported:

type Product {
  // Simple map
  attributes: map<string, string>

  // Nested map - directly supported
  nested_metadata: map<string, map<string, string>>

  // Triple nested map - also supported
  deep_config: map<string, map<string, map<string, int32>>>
}

Each generator handles nested maps appropriately:

Services and Methods

Services define RPC-style API methods. Each method takes an input type and returns an output type.

type GetProductRequest {
  id: string
}

type CreateProductRequest {
  name: string
  price: float64
  category: ProductCategory
}

type ListProductsRequest {
  category: ProductCategory
  limit: int32
  offset: int32
}

type ListProductsResponse {
  products: []Product
  total: int32
}

service ProductService {
  rpc GetProduct(GetProductRequest) returns (Product)
  rpc CreateProduct(CreateProductRequest) returns (Product)
  rpc ListProducts(ListProductsRequest) returns (ListProductsResponse)
}

Annotations and Attributes

Annotations add metadata to control code generation.

Field Attributes

@required

Mark fields as required (non-nullable):

type Product {
  id: string @required
  name: string @required
  price: float64 @required
  inStock: bool @required
  category: ProductCategory
  supplier: Supplier
  tags: []string
  images: []string
  attributes: map<string, string>
}

@default

Provide default values:

type Product {
  id: string @required
  name: string @required
  price: float64 @required
  inStock: bool @default("true")
  category: ProductCategory @default("OTHER")
  supplier: Supplier
  tags: []string
  images: []string
  attributes: map<string, string>
}

@exclude

Exclude fields from specific formats:

type Product {
  id: string @required
  name: string @required
  price: float64 @required
  inStock: bool @default("true")
  category: ProductCategory @default("OTHER")
  supplier: Supplier
  tags: []string
  images: []string
  attributes: map<string, string>
  internalNotes: string @exclude(graphql,openapi)
}

The internalNotes field will only appear in Protobuf.

@only

Include fields only in specific formats:

type Product {
  id: string @required
  name: string @required
  price: float64 @required
  inStock: bool @default("true")
  category: ProductCategory @default("OTHER")
  supplier: Supplier
  tags: []string
  images: []string
  attributes: map<string, string>
  internalNotes: string @exclude(graphql,openapi)
  graphqlMetadata: string @only(graphql)
}

Method Annotations

@http

Specify HTTP method:

service ProductService {
  rpc GetProduct(GetProductRequest) returns (Product)
    @http(GET)

  rpc CreateProduct(CreateProductRequest) returns (Product)
    @http(POST)

  rpc UpdateProduct(UpdateProductRequest) returns (Product)
    @http(PUT)

  rpc DeleteProduct(DeleteProductRequest) returns (DeleteProductResponse)
    @http(DELETE)
}

@path

Define URL path templates:

service ProductService {
  rpc GetProduct(GetProductRequest) returns (Product)
    @http(GET)
    @path("/api/v1/products/{id}")

  rpc CreateProduct(CreateProductRequest) returns (Product)
    @http(POST)
    @path("/api/v1/products")

  rpc UpdateProduct(UpdateProductRequest) returns (Product)
    @http(PUT)
    @path("/api/v1/products/{id}")

  rpc DeleteProduct(DeleteProductRequest) returns (DeleteProductResponse)
    @http(DELETE)
    @path("/api/v1/products/{id}")
}

@graphql

Specify GraphQL operation type:

service ProductService {
  rpc GetProduct(GetProductRequest) returns (Product)
    @http(GET)
    @path("/api/v1/products/{id}")
    @graphql(query)

  rpc CreateProduct(CreateProductRequest) returns (Product)
    @http(POST)
    @path("/api/v1/products")
    @graphql(mutation)

  rpc UpdateProduct(UpdateProductRequest) returns (Product)
    @http(PUT)
    @path("/api/v1/products/{id}")
    @graphql(mutation)

  rpc DeleteProduct(DeleteProductRequest) returns (DeleteProductResponse)
    @http(DELETE)
    @path("/api/v1/products/{id}")
    @graphql(mutation)
}

@success and @errors

Define HTTP status codes:

service ProductService {
  rpc CreateProduct(CreateProductRequest) returns (Product)
    @http(POST)
    @path("/api/v1/products")
    @graphql(mutation)
    @success(201)
    @errors(400,409,500)

  rpc GetProduct(GetProductRequest) returns (Product)
    @http(GET)
    @path("/api/v1/products/{id}")
    @graphql(query)
    @success(200)
    @errors(404,500)
}

Union Types

Unions represent a value that can be one of several types. This is useful for polymorphic responses.

type TextMessage {
  text: string @required
  timestamp: timestamp @required
}

type ImageMessage {
  imageUrl: string @required
  caption: string
  timestamp: timestamp @required
}

type VideoMessage {
  videoUrl: string @required
  thumbnail: string
  duration: int32
  timestamp: timestamp @required
}

union Message {
  TextMessage
  ImageMessage
  VideoMessage
}

type GetMessageRequest {
  id: string @required
}

service MessageService {
  rpc GetMessage(GetMessageRequest) returns (Message)
    @http(GET)
    @path("/api/v1/messages/{id}")
    @graphql(query)
}

Generated GraphQL:

union Message @oneOf = TextMessage | ImageMessage | VideoMessage

Generated Protobuf:

message Message {
  oneof value {
    TextMessage text_message = 1;
    ImageMessage image_message = 2;
    VideoMessage video_message = 3;
  }
}

Generated OpenAPI:

Message:
  oneOf:
    - $ref: '#/components/schemas/TextMessage'
    - $ref: '#/components/schemas/ImageMessage'
    - $ref: '#/components/schemas/VideoMessage'

Namespaces

Namespaces organize types and prevent naming conflicts. Use reverse domain notation:

namespace com.example.ecommerce

type Product {
  id: string @required
  name: string @required
}

Multiple Types with Same Name

Different namespaces can have types with the same name:

users.typemux:

namespace com.example.users

type User {
  id: string @required
  email: string @required
}

type Address {
  street: string @required
  city: string @required
}

orders.typemux:

namespace com.example.orders

type Order {
  id: string @required
  userId: string @required
}

type Address {
  latitude: float64 @required
  longitude: float64 @required
}

Both namespaces have an Address type, but they’re distinct.

Protobuf Generation with Namespaces

When using namespaces, Protobuf generates separate files per namespace:

./typemux -input schema.typemux -format protobuf -output ./generated

Creates:

Multiple Files and Imports

Split large schemas into multiple files using imports.

types.typemux:

type User {
  id: string @required
  email: string @required
}

type Product {
  id: string @required
  name: string @required
}

services.typemux:

import "types.typemux"

type GetUserRequest {
  id: string @required
}

type GetProductRequest {
  id: string @required
}

service UserService {
  rpc GetUser(GetUserRequest) returns (User)
    @http(GET)
    @path("/users/{id}")
}

service ProductService {
  rpc GetProduct(GetProductRequest) returns (Product)
    @http(GET)
    @path("/products/{id}")
}

Cross-Namespace Imports

You can import types from different namespaces:

com/example/users/types.typemux:

namespace com.example.users

type User {
  id: string @required
  email: string @required
}

com/example/orders/types.typemux:

namespace com.example.orders

import "com/example/users/types.typemux"

type Order {
  id: string @required
  userId: string @required
  user: User
}

External YAML Annotations

For large projects, you can separate annotations from your schema definition using YAML files.

schema.typemux:

type Product {
  id: string
  name: string
  price: float64
  inStock: bool
}

service ProductService {
  rpc GetProduct(GetProductRequest) returns (Product)
  rpc CreateProduct(CreateProductRequest) returns (Product)
}

annotations.yaml:

types:
  Product:
    fields:
      id:
        required: true
      name:
        required: true
      price:
        required: true
      inStock:
        required: true
        default: true

services:
  ProductService:
    methods:
      GetProduct:
        http: GET
        path: /api/v1/products/{id}
        graphql: query
        success: [200]
        errors: [404, 500]
      CreateProduct:
        http: POST
        path: /api/v1/products
        graphql: mutation
        success: [201]
        errors: [400, 500]

Generate with annotations:

./typemux -input schema.typemux -annotations annotations.yaml -output ./generated

Multiple Annotation Files

You can merge multiple annotation files. Later files override earlier ones:

./typemux -input schema.typemux \
          -annotations base-annotations.yaml \
          -annotations overrides.yaml \
          -output ./generated

Format-Specific Names

Override type and field names per format:

annotations.yaml:

types:
  Product:
    proto:
      name: ProductMessage
    graphql:
      name: ProductType
    openapi:
      name: ProductSchema
    fields:
      id:
        proto:
          name: product_id
        graphql:
          name: productId

Qualified Names in Namespaces

When using namespaces, use fully qualified names in YAML:

annotations.yaml:

types:
  com.example.users.User:
    fields:
      id:
        required: true

  com.example.orders.Order:
    fields:
      id:
        required: true
      userId:
        required: true

Documentation Comments

Use triple-slash comments (///) to add documentation:

/// User account with authentication details
type User {
  /// Unique user identifier
  id: string @required

  /// User's email address for login
  email: string @required

  /// Display name
  username: string @required
}

/// Service for managing user accounts
service UserService {
  /// Retrieves a user by their unique ID
  rpc GetUser(GetUserRequest) returns (User)
    @http(GET)
    @path("/api/v1/users/{id}")
    @graphql(query)
}

Documentation is included in all generated formats:

GraphQL:

"""
User account with authentication details
"""
type User {
  """
  Unique user identifier
  """
  id: String!
}

Protobuf:

// User account with authentication details
message User {
  // Unique user identifier
  string id = 1;
}

OpenAPI:

User:
  type: object
  description: User account with authentication details
  properties:
    id:
      type: string
      description: Unique user identifier

Custom Field Numbers

Control Protobuf field numbering explicitly:

type Product {
  id: string = 1
  name: string = 2
  price: float64 = 10
  category: ProductCategory = 100
}

enum ProductCategory {
  ELECTRONICS = 1
  CLOTHING = 2
  BOOKS = 3
}

This is useful for:

Complete Example

Here’s a complete e-commerce schema combining all concepts:

ecommerce.typemux:

namespace com.example.ecommerce

/// Product category enumeration
enum ProductCategory {
  ELECTRONICS = 1
  CLOTHING = 2
  BOOKS = 3
  FOOD = 4
  OTHER = 5
}

/// Product availability status
enum ProductStatus {
  AVAILABLE = 1
  OUT_OF_STOCK = 2
  DISCONTINUED = 3
}

/// Product supplier information
type Supplier {
  /// Unique supplier identifier
  id: string @required

  /// Supplier company name
  name: string @required

  /// Contact email
  email: string @required

  /// Contact phone number
  phone: string
}

/// Product in the catalog
type Product {
  /// Unique product identifier
  id: string @required

  /// Product name
  name: string @required

  /// Product description
  description: string

  /// Price in USD
  price: float64 @required

  /// Product category
  category: ProductCategory @required

  /// Current availability status
  status: ProductStatus @default("AVAILABLE")

  /// Product supplier
  supplier: Supplier

  /// Search tags
  tags: []string

  /// Product image URLs
  images: []string

  /// Custom attributes
  attributes: map<string, string>

  /// Creation timestamp
  createdAt: timestamp @required

  /// Last update timestamp
  updatedAt: timestamp @required
}

/// Request to retrieve a product by ID
type GetProductRequest {
  /// Product identifier
  id: string @required
}

/// Request to create a new product
type CreateProductRequest {
  name: string @required
  description: string
  price: float64 @required
  category: ProductCategory @required
  supplierId: string @required
  tags: []string
  images: []string
  attributes: map<string, string>
}

/// Request to list products with filtering
type ListProductsRequest {
  /// Filter by category
  category: ProductCategory

  /// Filter by status
  status: ProductStatus

  /// Maximum number of results
  limit: int32 @default("50")

  /// Offset for pagination
  offset: int32 @default("0")
}

/// Response containing product list
type ListProductsResponse {
  /// List of products
  products: []Product @required

  /// Total count of products matching filter
  total: int32 @required
}

/// Product catalog service
service ProductService {
  /// Get a product by its ID
  rpc GetProduct(GetProductRequest) returns (Product)
    @http(GET)
    @path("/api/v1/products/{id}")
    @graphql(query)
    @success(200)
    @errors(404,500)

  /// Create a new product
  rpc CreateProduct(CreateProductRequest) returns (Product)
    @http(POST)
    @path("/api/v1/products")
    @graphql(mutation)
    @success(201)
    @errors(400,500)

  /// List products with optional filtering
  rpc ListProducts(ListProductsRequest) returns (ListProductsResponse)
    @http(GET)
    @path("/api/v1/products")
    @graphql(query)
    @success(200)
    @errors(500)
}

Generate all formats:

./typemux -input ecommerce.typemux -output ./generated

Next Steps