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.
✨ 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
@useAuthdecorators - 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
npm install @murbagus/typespec-domaingin-emitter- Install dependencies:
npm install @typespec/compiler @typespec/http @murbagus/typespec-domaingin-emitter- 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"- 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;
};
}- Compile:
npx tsp compile .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"})
}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"})
}The v2.0 introduces powerful decorator-based generation control that provides more flexibility than comment-based generation.
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
}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(...): ...;The emitter follows this priority order:
-
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
- If any operation has
-
Namespace-level decorators (medium priority)
- If namespace has
@domainGinHandlerGenand no operations have decorators, all operations in namespace are generated
- If namespace has
-
Comment-based generation (lowest priority - backward compatibility)
- Falls back to
//!Generatecomment or configuredgenerate-commentoption
- Falls back to
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 names are resolved in this order:
- Operation-level
@domainGinHandlerName(highest priority) - Namespace-level
@domainGinHandlerName - 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"
| 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 |
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)
}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
}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
}Only files marked with the generation comment are regenerated. New handlers are appended to existing files.
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"`
// }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
}The emitter can detect multipart requests and generate appropriate form tags alongside JSON tags.
- Organize TypeSpec files: Keep models in
/models/and routes in/routes/ - Use descriptive comments: Add documentation to your TypeSpec definitions
- Mark selectively: Only add generation comments to routes you want to regenerate
- Preserve custom code: Keep your implementations above the separator in model files
- Uncomment safely: Generated code is commented for safety - uncomment and modify as needed
- Follow Gin patterns: Generated handlers follow
github.com/gin-gonic/ginconventions - Version control: Commit both TypeSpec files and generated Go code
We welcome contributions! Please see our Contributing Guide for details.
MIT License - see LICENSE file for details.
We welcome contributions! Please see our Contributing Guide for details.
MIT License - see LICENSE file for details.
Made with ❤️ by Muhammad Refy for the Go and TypeSpec communities.