Skip to content

Instantly share code, notes, and snippets.

@17twenty
Last active May 29, 2025 08:11
Show Gist options
  • Save 17twenty/346db477b73867c51d80e844004fea36 to your computer and use it in GitHub Desktop.
Save 17twenty/346db477b73867c51d80e844004fea36 to your computer and use it in GitHub Desktop.
Create a Golang server from TypeAPI JSON
{
"operations": {
"user.getById": {
"description": "Get user profile by ID",
"method": "GET",
"path": "/users/{id}",
"arguments": {
"id": {
"in": "path",
"schema": {
"type": "integer"
}
}
},
"return": {
"schema": {
"type": "reference",
"target": "UserProfile"
}
},
"throws": [{
"code": 404,
"schema": {
"type": "reference",
"target": "Error"
}
}]
},
"user.create": {
"description": "Create a new user",
"method": "POST",
"path": "/users",
"arguments": {
"payload": {
"in": "body",
"schema": {
"type": "reference",
"target": "CreateUserRequest"
}
}
},
"return": {
"schema": {
"type": "reference",
"target": "UserProfile"
}
},
"throws": [{
"code": 400,
"schema": {
"type": "reference",
"target": "Error"
}
}]
},
"user.list": {
"description": "List users with pagination",
"method": "GET",
"path": "/users",
"arguments": {
"limit": {
"in": "query",
"schema": {
"type": "integer"
}
},
"offset": {
"in": "query",
"schema": {
"type": "integer"
}
}
},
"return": {
"schema": {
"type": "reference",
"target": "UserList"
}
}
},
"health.getData": {
"description": "Get user health data",
"method": "GET",
"path": "/users/{userId}/health",
"arguments": {
"userId": {
"in": "path",
"schema": {
"type": "integer"
}
}
},
"return": {
"schema": {
"type": "reference",
"target": "HealthData"
}
},
"throws": [{
"code": 404,
"schema": {
"type": "reference",
"target": "Error"
}
}]
}
},
"definitions": {
"UserProfile": {
"type": "struct",
"properties": {
"id": {
"type": "integer"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"dateOfBirth": {
"type": "string"
},
"bioGender": {
"type": "string"
},
"height": {
"type": "number"
},
"weight": {
"type": "number"
},
"goal": {
"type": "string"
},
"isEmailVerified": {
"type": "boolean"
},
"createdAt": {
"type": "string"
}
}
},
"CreateUserRequest": {
"type": "struct",
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"password": {
"type": "string"
},
"dateOfBirth": {
"type": "string"
},
"bioGender": {
"type": "string"
}
}
},
"UserList": {
"type": "struct",
"properties": {
"users": {
"type": "array",
"items": {
"type": "reference",
"target": "UserProfile"
}
},
"total": {
"type": "integer"
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
}
}
},
"HealthData": {
"type": "struct",
"properties": {
"userId": {
"type": "integer"
},
"existingMedications": {
"type": "string"
},
"medicalConditions": {
"type": "string"
},
"allergies": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"Error": {
"type": "struct",
"properties": {
"code": {
"type": "integer"
},
"message": {
"type": "string"
},
"details": {
"type": "string"
}
}
}
},
"security": {
"type": "httpBearer"
}
}
{"operations":{"user.getById":{"description":"Get user profile by ID","method":"GET","path":"/users/{id}","arguments":{"id":{"in":"path","schema":{"type":"integer"}}},"return":{"schema":{"type":"reference","target":"UserProfile"}},"throws":[{"code":404,"schema":{"type":"reference","target":"Error"}}]},"user.create":{"description":"Create a new user","method":"POST","path":"/users","arguments":{"payload":{"in":"body","schema":{"type":"reference","target":"CreateUserRequest"}}},"return":{"schema":{"type":"reference","target":"UserProfile"}},"throws":[{"code":400,"schema":{"type":"reference","target":"Error"}}]},"user.list":{"description":"List users with pagination","method":"GET","path":"/users","arguments":{"limit":{"in":"query","schema":{"type":"integer"}},"offset":{"in":"query","schema":{"type":"integer"}}},"return":{"schema":{"type":"reference","target":"UserList"}}}},"definitions":{"UserProfile":{"type":"struct","properties":{"id":{"type":"integer"},"email":{"type":"string"},"name":{"type":"string"},"dateOfBirth":{"type":"string"},"bioGender":{"type":"string"}}},"CreateUserRequest":{"type":"struct","properties":{"email":{"type":"string"},"name":{"type":"string"}}},"UserList":{"type":"struct","properties":{"users":{"type":"array","items":{"type":"reference","target":"UserProfile"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"Error":{"type":"struct","properties":{"code":{"type":"integer"},"message":{"type":"string"},"details":{"type":"string"}}}},"security":{"type":"httpBearer"}}
package main
// Run me like this:
// go run typeapi-gen.go "$SPEC_FILE" "$OUTPUT_DIR"
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
)
type TypeAPISpec struct {
Operations map[string]Operation `json:"operations"`
Definitions map[string]Definition `json:"definitions"`
Security *Security `json:"security,omitempty"`
}
type Operation struct {
Description string `json:"description,omitempty"`
Method string `json:"method"`
Path string `json:"path"`
Arguments map[string]Argument `json:"arguments,omitempty"`
Return *Return `json:"return,omitempty"`
Throws []ThrowsSpec `json:"throws,omitempty"`
}
type Argument struct {
In string `json:"in"`
Schema Schema `json:"schema"`
}
type Return struct {
Schema Schema `json:"schema"`
}
type ThrowsSpec struct {
Code int `json:"code"`
Schema Schema `json:"schema"`
}
type Schema struct {
Type string `json:"type"`
Target string `json:"target,omitempty"`
Properties map[string]Schema `json:"properties,omitempty"`
Items *Schema `json:"items,omitempty"`
}
type Definition struct {
Type string `json:"type"`
Properties map[string]Schema `json:"properties,omitempty"`
}
type Security struct {
Type string `json:"type"`
In string `json:"in,omitempty"`
Name string `json:"name,omitempty"`
TokenURL string `json:"tokenUrl,omitempty"`
AuthorizationURL string `json:"authorizationUrl,omitempty"`
Scopes []string `json:"scopes,omitempty"`
}
type GeneratedHandler struct {
Name string
Method string
Path string
Description string
Args []HandlerArg
ReturnType string
Errors []ErrorCase
}
type HandlerArg struct {
Name string
Type string
Source string // "query", "path", "body", "header"
GoType string
}
type ErrorCase struct {
Code int
Type string
}
const handlerTemplate = `package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
{{range .Definitions}}
{{.}}
{{end}}
{{range .Handlers}}
// {{.Description}}
func {{.Name}}Handler(w http.ResponseWriter, r *http.Request) {
{{range .Args}}
{{if eq .Source "query"}}
{{.Name}}Str := r.URL.Query().Get("{{.Name}}")
{{if eq .GoType "int"}}
var {{.Name}} int
if {{.Name}}Str != "" {
var err error
{{.Name}}, err = strconv.Atoi({{.Name}}Str)
if err != nil {
http.Error(w, "Invalid {{.Name}} parameter", http.StatusBadRequest)
return
}
}
{{else if eq .GoType "string"}}
{{.Name}} := {{.Name}}Str
{{end}}
{{else if eq .Source "path"}}
{{.Name}}Str := mux.Vars(r)["{{.Name}}"]
{{if eq .GoType "int"}}
{{.Name}}, err := strconv.Atoi({{.Name}}Str)
if err != nil {
http.Error(w, "Invalid {{.Name}} parameter", http.StatusBadRequest)
return
}
{{else if eq .GoType "string"}}
{{.Name}} := {{.Name}}Str
{{end}}
{{else if eq .Source "body"}}
var {{.Name}} {{.GoType}}
if err := json.NewDecoder(r.Body).Decode(&{{.Name}}); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
{{end}}
{{end}}
// TODO: Implement your business logic here
// Call your service layer with the parsed arguments
{{if .ReturnType}}
result := {{.ReturnType}}{}
// Example implementation:
// result, err := yourService.{{.Name}}({{range $i, $arg := .Args}}{{if $i}}, {{end}}{{$arg.Name}}{{end}})
// if err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// return
// }
{{end}}
// Use variables to prevent "declared and not used" errors
{{range .Args}}
_ = {{.Name}}
{{end}}
w.Header().Set("Content-Type", "application/json")
{{if .ReturnType}}
if err := json.NewEncoder(w).Encode(result); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
{{else}}
w.WriteHeader(http.StatusOK)
{{end}}
}
{{end}}
func SetupRoutes() *mux.Router {
router := mux.NewRouter()
{{range .Handlers}}
router.HandleFunc("{{.Path}}", {{.Name}}Handler).Methods("{{.Method}}")
{{end}}
return router
}
func main() {
router := SetupRoutes()
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", router))
}
`
func main() {
if len(os.Args) < 2 {
log.Fatal("Usage: typeapi-gen <spec-file> [output-dir]")
}
specFile := os.Args[1]
outputDir := "."
if len(os.Args) > 2 {
outputDir = os.Args[2]
}
spec, err := parseTypeAPISpec(specFile)
if err != nil {
log.Fatalf("Failed to parse TypeAPI spec: %v", err)
}
generator := &Generator{spec: spec}
if err := generator.Generate(outputDir); err != nil {
log.Fatalf("Failed to generate code: %v", err)
}
fmt.Println("Successfully generated Go server code")
}
func parseTypeAPISpec(filename string) (*TypeAPISpec, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return nil, err
}
var spec TypeAPISpec
if err := json.Unmarshal(data, &spec); err != nil {
return nil, err
}
return &spec, nil
}
type Generator struct {
spec *TypeAPISpec
}
func (g *Generator) Generate(outputDir string) error {
handlers := g.generateHandlers()
definitions := g.generateDefinitions()
data := struct {
Handlers []GeneratedHandler
Definitions []string
}{
Handlers: handlers,
Definitions: definitions,
}
tmpl, err := template.New("handlers").Parse(handlerTemplate)
if err != nil {
return err
}
outputFile := filepath.Join(outputDir, "generated_server.go")
file, err := os.Create(outputFile)
if err != nil {
return err
}
defer file.Close()
return tmpl.Execute(file, data)
}
func (g *Generator) generateHandlers() []GeneratedHandler {
var handlers []GeneratedHandler
// Sort operation names for consistent output
var opNames []string
for name := range g.spec.Operations {
opNames = append(opNames, name)
}
sort.Strings(opNames)
for _, opName := range opNames {
op := g.spec.Operations[opName]
handler := GeneratedHandler{
Name: toGoFunctionName(opName),
Method: strings.ToUpper(op.Method),
Path: g.convertPath(op.Path),
Description: op.Description,
Args: g.generateArguments(op.Arguments),
ReturnType: g.getReturnType(op.Return),
Errors: g.generateErrors(op.Throws),
}
handlers = append(handlers, handler)
}
return handlers
}
func (g *Generator) generateArguments(args map[string]Argument) []HandlerArg {
var handlerArgs []HandlerArg
for name, arg := range args {
handlerArg := HandlerArg{
Name: name,
Type: arg.Schema.Type,
Source: arg.In,
GoType: g.schemaToGoType(arg.Schema),
}
handlerArgs = append(handlerArgs, handlerArg)
}
return handlerArgs
}
func (g *Generator) generateErrors(throws []ThrowsSpec) []ErrorCase {
var errors []ErrorCase
for _, t := range throws {
errors = append(errors, ErrorCase{
Code: t.Code,
Type: g.schemaToGoType(t.Schema),
})
}
return errors
}
func (g *Generator) getReturnType(ret *Return) string {
if ret == nil {
return ""
}
return g.schemaToGoType(ret.Schema)
}
func (g *Generator) generateDefinitions() []string {
var definitions []string
// Sort definition names for consistent output
var defNames []string
for name := range g.spec.Definitions {
defNames = append(defNames, name)
}
sort.Strings(defNames)
for _, name := range defNames {
def := g.spec.Definitions[name]
goStruct := g.generateStruct(name, def)
definitions = append(definitions, goStruct)
}
return definitions
}
func (g *Generator) generateStruct(name string, def Definition) string {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("type %s struct {\n", toGoTypeName(name)))
// Sort property names for consistent output
var propNames []string
for propName := range def.Properties {
propNames = append(propNames, propName)
}
sort.Strings(propNames)
for _, propName := range propNames {
prop := def.Properties[propName]
goType := g.schemaToGoType(prop)
jsonTag := fmt.Sprintf("`json:\"%s\"`", propName)
builder.WriteString(fmt.Sprintf("\t%s %s %s\n", toGoFieldName(propName), goType, jsonTag))
}
builder.WriteString("}")
return builder.String()
}
func (g *Generator) schemaToGoType(schema Schema) string {
switch schema.Type {
case "string":
return "string"
case "integer":
return "int"
case "number":
return "float64"
case "boolean":
return "bool"
case "array":
if schema.Items != nil {
itemType := g.schemaToGoType(*schema.Items)
return "[]" + itemType
}
return "[]interface{}"
case "reference":
if schema.Target != "" {
return toGoTypeName(schema.Target)
}
return "interface{}"
default:
return "interface{}"
}
}
func (g *Generator) convertPath(path string) string {
// Convert TypeAPI path parameters to Gorilla mux format
// e.g., /users/{id} stays as /users/{id}
return path
}
func toPascalCase(s string) string {
// Split by common delimiters
parts := strings.FieldsFunc(s, func(c rune) bool {
return c == '_' || c == '-' || c == '.' || c == ' '
})
var result strings.Builder
for _, part := range parts {
if len(part) > 0 {
result.WriteString(strings.ToUpper(string(part[0])))
if len(part) > 1 {
result.WriteString(strings.ToLower(part[1:]))
}
}
}
return result.String()
}
func toGoTypeName(s string) string {
return toPascalCase(s)
}
func toGoFieldName(s string) string {
return toPascalCase(s)
}
func toGoFunctionName(s string) string {
// Convert operation names like "user.getById" to "UserGetById"
return toPascalCase(s)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment