Skip to content

TypeSpec emitter that generates Go domain logic and Gin HTTP handlers following my own project structure and coding standards

License

Notifications You must be signed in to change notification settings

murbagus/typespec-domaingin-emitter

Repository files navigation

TypeSpec Go Gin Emitter

npm version license downloads

A TypeSpec emitter that generates Go structs and HTTP handlers specifically designed for github.com/gin-gonic/gin framework applications. This emitter produces handler code that follows Gin's conventions and patterns.

Features

Domain Models Generation

  • Generate Go structs from TypeSpec models
  • Automatic field name conversion to PascalCase
  • JSON tags from TypeSpec field names
  • Support for nullable types using github.com/guregu/null/v6
  • Preserve existing code with comment separators

🔧 Gin-Compatible Handler Generation

  • Generate HTTP handlers compatible with github.com/gin-gonic/gin
  • Inline request struct definitions (matching Gin best practices)
  • Automatic parameter detection (path, query, body)
  • Authentication comment generation from @useAuth decorators
  • Configurable validation tags from TypeSpec decorators
  • Commented request variables for safety and flexibility
  • Clean template-based generation without binding code clutter

🎯 Smart Generation Control (New in v2.0)

  • Decorator-based generation control with @domainGinHandlerGen
  • Custom handler names with @domainGinHandlerName("CustomHandler")
  • Namespace-level or operation-level generation
  • Backward compatible comment-based generation
  • Selective file regeneration
  • Incremental updates without losing existing code
  • Support for multipart/form-data requests

Installation

npm install @murbagus/typespec-domaingin-emitter

Quick Start

  1. Install dependencies:
npm install @typespec/compiler @typespec/http @murbagus/typespec-domaingin-emitter
  1. Configure tspconfig.yaml:
emit:
  - "@murbagus/typespec-domaingin-emitter"
options:
  "@murbagus/typespec-domaingin-emitter":
    emitter-output-dir: "{project-root}/domain"
    handler-output-dir: "{project-root}/handlers"
  1. Use decorators for generation control:
// routes/users.tsp
import "@typespec/http";
import "@murbagus/typespec-domaingin-emitter";

using Http;

// Generate all operations in this namespace with custom handler name
@domainGinHandlerGen
@domainGinHandlerName("UserHandler")
@route("/users")
@tag("Users")
namespace UserAPI {
    model CreateUserRequest {
        @maxLength(100)
        name: string;

        @minLength(3)
        @maxLength(50)
        username: string;

        email?: string;
    }

    @post
    op CreateUser(@body request: CreateUserRequest): {
        @statusCode statusCode: 201;
        @body user: User;
    };

    @get
    op ListUsers(): {
        @statusCode statusCode: 200;
        @body users: User[];
    };
}

// Only generate specific operations
@route("/auth")
namespace AuthAPI {
    @post
    @domainGinHandlerGen
    @domainGinHandlerName("AuthHandler")
    op Login(@body request: LoginRequest): {
        @statusCode statusCode: 200;
        @body token: string;
    };

    // This operation won't be generated (no decorator)
    @post
    op Register(@body request: RegisterRequest): {
        @statusCode statusCode: 201;
        @body user: User;
    };
}
  1. Compile:
npx tsp compile .

Generated Output

Namespace-Level Generation

From the example above, UserAPI namespace will generate:

package http

// CreateUser handles the createuser operation
// Response: Expected response type based on operation definition
func (h *UserHandler) CreateUser(c *gin.Context) {
    // var request struct {
    //     Name     string      `json:"name" binding:"required,max=100"`
    //     Username string      `json:"username" binding:"required,min=3,max=50"`
    //     Email    null.String `json:"email"`
    // }

    // TODO: Implement your handler logic for CreateUser here 🚀 (by Muhammad Refy)
    c.JSON(200, gin.H{"message": "Not implemented"})
}

// ListUsers handles the listusers operation
// Response: Expected response type based on operation definition
func (h *UserHandler) ListUsers(c *gin.Context) {
    // TODO: Implement your handler logic for ListUsers here 🚀 (by Muhammad Refy)
    c.JSON(200, gin.H{"message": "Not implemented"})
}

Operation-Level Generation

Only the Login operation will be generated:

package http

// Login handles the login operation
// Response: Expected response type based on operation definition
func (h *AuthHandler) Login(c *gin.Context) {
    // var request struct {
    //     Username string `json:"username" binding:"required"`
    //     Password string `json:"password" binding:"required"`
    // }

    // TODO: Implement your handler logic for Login here 🚀 (by Muhammad Refy)
    c.JSON(200, gin.H{"message": "Not implemented"})
}

Decorator-Based Generation Control (v2.0)

The v2.0 introduces powerful decorator-based generation control that provides more flexibility than comment-based generation.

Available Decorators

@domainGinHandlerGen

Marks a namespace or operation for handler generation.

Namespace-level usage:

@domainGinHandlerGen
@route("/users")
namespace UserAPI {
    // All operations in this namespace will be generated
    @post op CreateUser(...): ...;
    @get op ListUsers(...): ...;
}

Operation-level usage:

@route("/auth")
namespace AuthAPI {
    @domainGinHandlerGen
    @post op Login(...): ...;  // Only this operation will be generated

    @post op Register(...): ...; // This will NOT be generated
}

@domainGinHandlerName("HandlerName")

Specifies the custom handler struct name for generated functions.

Examples:

// Generates: func (h *UserHandler) CreateUser(c *gin.Context)
@domainGinHandlerGen
@domainGinHandlerName("UserHandler")
namespace UserAPI {
    @post op CreateUser(...): ...;
}

// Generates: func (h *AuthHandler) Login(c *gin.Context)
@post
@domainGinHandlerGen
@domainGinHandlerName("AuthHandler")
op Login(...): ...;

Generation Precedence

The emitter follows this priority order:

  1. Operation-level decorators (highest priority)

    • If any operation has @domainGinHandlerGen, only those operations are generated
    • Namespace-level decorators are ignored when operation-level decorators exist
  2. Namespace-level decorators (medium priority)

    • If namespace has @domainGinHandlerGen and no operations have decorators, all operations in namespace are generated
  3. Comment-based generation (lowest priority - backward compatibility)

    • Falls back to //!Generate comment or configured generate-comment option

Examples

Mixed Usage Example

import "@typespec/http";
import "@murbagus/typespec-domaingin-emitter";

using Http;

// Namespace with decorator - generates ALL operations with UserHandler
@domainGinHandlerGen
@domainGinHandlerName("UserHandler")
@route("/users")
namespace UserAPI {
    @post op CreateUser(@body request: CreateUserRequest): User;
    @get op GetUser(@path id: int32): User;
    @put op UpdateUser(@path id: int32, @body request: UpdateUserRequest): User;
    @delete op DeleteUser(@path id: int32): void;
}

// Mixed namespace - only Login is generated because of operation-level decorator
@route("/auth")
namespace AuthAPI {
    @post
    @domainGinHandlerGen
    @domainGinHandlerName("AuthHandler")
    op Login(@body request: LoginRequest): LoginResponse;

    @post
    op Register(@body request: RegisterRequest): User; // NOT generated

    @post
    op ForgotPassword(@body request: ForgotPasswordRequest): void; // NOT generated
}

// Legacy comment-based generation (still supported)
//!Generate
@route("/products")
namespace ProductAPI {
    @post op CreateProduct(@body request: ProductRequest): Product;
    @get op ListProducts(): Product[];
}

Handler Name Priority

Handler names are resolved in this order:

  1. Operation-level @domainGinHandlerName (highest priority)
  2. Namespace-level @domainGinHandlerName
  3. Default Handler (fallback)
@domainGinHandlerName("UserService")  // Namespace-level name
@route("/users")
namespace UserAPI {
    @post
    @domainGinHandlerName("UserController")  // Operation-level name (takes precedence)
    op CreateUser(...): ...;  // Generated as: func (h *UserController) CreateUser(...)

    @get
    op GetUser(...): ...;     // Generated as: func (h *UserService) GetUser(...)
}
c.JSON(200, gin.H{"message": "Not implemented"})

}


## Configuration Options

| Option               | Type   | Default                      | Description                                  |
| -------------------- | ------ | ---------------------------- | -------------------------------------------- |
| `emitter-output-dir` | string | `"../application/domain"`    | Output directory for domain models           |
| `handler-output-dir` | string | `"../drivers/delivery/http"` | Output directory for HTTP handlers           |
| `generate-comment`   | string | `"//!Generate"`              | Comment to mark files for handler generation |

### Example Configuration

```yaml
# tspconfig.yaml
emit:
  - "typespec-domaingin-emitter"
options:
  "typespec-domaingin-emitter":
    emitter-output-dir: "./internal/domain"
    handler-output-dir: "./internal/handlers"
    generate-comment: "// @generate"

Type Mapping

TypeSpec Type Optional Go Type
string No string
string? Yes null.String
int32 No int32
int32? Yes null.Int
utcDateTime No time.Time
utcDateTime? Yes null.Time
boolean No bool
boolean? Yes null.Bool
ModelName No ModelName
ModelName? Yes *ModelName

Validation Tags

TypeSpec decorators are automatically converted to Go validation tags:

model User {
    @maxLength(100)
    name: string;           // → `binding:"required,max=100"`

    @minLength(8)
    @maxLength(128)
    password: string;       // → `binding:"required,min=8,max=128"`

    @minItems(1)
    tags: string[];         // → `binding:"required,min=1,dive,required"`

    email?: string;         // → `binding:""` (optional, no required tag)
}

Authentication Support

The emitter recognizes @useAuth decorators and generates appropriate comments:

@useAuth(BearerAuth)
@get
op GetUser(@path id: int32): User;

Generates:

// GetUser retrieves user information
// Authentication: Required Bearer Token authentication
// Path parameters:
// - id: path parameter
func (h *Handler) GetUser(c *gin.Context) {
    // id := c.Param("id") // int32
    // TODO: Implement GetUser handler logic
}

Incremental Generation

Models

Domain models are always regenerated, but existing code above the separator comment is preserved:

package domain

import "time"

// Custom constants and functions
const UserStatusActive = 1

// --- Generated by typespec ---

// User represents the user data structure
type User struct {
    // ... generated fields
}

Handlers

Only files marked with the generation comment are regenerated. New handlers are appended to existing files.

Nested Structures

For complex request bodies with nested objects:

model Address {
    street: string;
    city: string;
    zipCode?: string;
}

model CreateUserRequest {
    name: string;
    addresses: Address[];
}

Generates inline anonymous structs (commented for safety):

// var request struct {
//     Name string `json:"name" binding:"required"`
//     Addresses []struct {
//         Street  string      `json:"street" binding:"required"`
//         City    string      `json:"city" binding:"required"`
//         ZipCode null.String `json:"zipCode"`
//     } `json:"addresses" binding:"required,dive,required"`
// }

Safety-First Approach

All generated request variables are commented out by default to prevent:

  • Conflicts with existing code
  • Unintended variable shadowing
  • Compilation errors in complex handlers

Simply uncomment and modify the generated code as needed:

func (h *Handler) CreateUser(c *gin.Context) {
    // Generated template (uncomment and modify as needed)
    // var request struct {
    //     Name string `json:"name" binding:"required"`
    // }

    // Your custom implementation
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        // handle error
    }
    // ... rest of implementation
}

Multipart Support

The emitter can detect multipart requests and generate appropriate form tags alongside JSON tags.

Best Practices

  1. Organize TypeSpec files: Keep models in /models/ and routes in /routes/
  2. Use descriptive comments: Add documentation to your TypeSpec definitions
  3. Mark selectively: Only add generation comments to routes you want to regenerate
  4. Preserve custom code: Keep your implementations above the separator in model files
  5. Uncomment safely: Generated code is commented for safety - uncomment and modify as needed
  6. Follow Gin patterns: Generated handlers follow github.com/gin-gonic/gin conventions
  7. Version control: Commit both TypeSpec files and generated Go code

Contributing

We welcome contributions! Please see our Contributing Guide for details.

License

MIT License - see LICENSE file for details.

Contributing

We welcome contributions! Please see our Contributing Guide for details.

License

MIT License - see LICENSE file for details.


Made with ❤️ by Muhammad Refy for the Go and TypeSpec communities.

About

TypeSpec emitter that generates Go domain logic and Gin HTTP handlers following my own project structure and coding standards

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published