TypeScript Client Generator
The tsclient
generator produces a typed client for your HTTP API. It automatically discovers all handler packages in a hierarchical structure, reads route declarations and builds functions that validate requests and responses using Zod .
Usage
bin/goframe generate client [packages...] --file <output>
Parameters:
packages
- Root handler packages underinternal
. If none provided, automatically discovers all root packages--file
- Output file path. Without this flag, results print to console
The generator automatically discovers root handler packages (containing router.go
and registry.go
) and all their subfolders.
Quick Start
bin/goframe generate client v1handler --file ./web/api.ts
The generated file exports:
Fetcher
type interface- Request and response Zod schemas
- Type-safe client functions (one per route)
- Error handling classes
Hierarchical Package Organization
The generator organizes handlers into namespaces based on their package hierarchy. It automatically resolves naming conflicts and creates clear namespace structures.
Namespace Rules
-
Root Package Handlers: Use handler name directly
// internal/v1handler/handler_user.go → UserHandler export namespace UserClient { export async function getUsers(request: GetUsersRequest){} }
-
Subpackage Handlers: Include package path as prefix
// internal/v1handler/dashboard/handler_stats.go → StatsHandler export namespace DashboardStatsClient { export async function getStats(request: GetStatsRequest){} } // internal/v1handler/admin/reports/handler_reports.go → ReportsHandler export namespace AdminReportsReportsClient { export async function getReports(request: GetReportsRequest){} }
-
Multiple Root Packages: Separate files generated
// internal/v1handler generates one file // internal/v2handler generates another file
-
Type Prefixing: Types are prefixed when conflicts occur between packages
export namespace UserClient { export async function getCompanies(request: GetCompaniesRequest){} } export namespace DashboardUserClient { export async function getCompanies(request: DashboardGetCompaniesRequest){} }
Route Declaration Syntax
Routes are declared using goframe:http_route
comments above handler functions.
Basic Route
// goframe:http_route path=/users method=GET response=UserResponse
func GetUsers() http.HandlerFunc {
// handler implementation
}
Auto-detected Types
If request
or response
are omitted, the generator automatically looks for types named {FunctionName}Request
and {FunctionName}Response
:
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
}
// goframe:http_route path=/users method=POST
func CreateUser() http.HandlerFunc {
// automatically uses CreateUserRequest and CreateUserResponse
}
Multiple HTTP Methods
// goframe:http_route path=/items method=[GET, POST] response=ItemResponse
func ManageItems() http.HandlerFunc {
// handles both GET and POST requests
}
Required Headers
// goframe:http_route path=/reports method=GET required_header=Authorization response=ReportResponse
func GetReports() http.HandlerFunc {
// requires Authorization header
}
Named Routes
// goframe:http_route path=/orders method=GET name=ListOrders response=OrderListResponse
func ListOrders() http.HandlerFunc {
// can be referenced as "ListOrders" in generated client
}
Advanced Response Handling
Status-Specific Responses
Define different response types for different HTTP status codes:
// goframe:http_route path=/login method=POST request=LoginRequest response=200:LoginSuccessResponse response=401:ErrorResponse
func Login() http.HandlerFunc {
// 200 responses → LoginSuccessResponse
// 401 responses → ErrorResponse
}
Wildcard Status Patterns
// goframe:http_route path=/process method=POST response=2xx:SuccessResponse response=4xx:ClientErrorResponse response=5xx:ServerErrorResponse
func Process() http.HandlerFunc {
// 2xx codes → SuccessResponse
// 4xx codes → ClientErrorResponse
// 5xx codes → ServerErrorResponse
}
Status Code Ranges
// goframe:http_route path=/status method=GET response=200-299:OKResponse response=400-499:ClientError response=500-599:ServerError
func CheckStatus() http.HandlerFunc {
// Range-based status handling
}
Special Response Types
Error
- Automatically maps to 4xx/5xx status codesRedirect
- Automatically maps to 3xx status codesTYPE_ERROR
- Explicit error response in status declarationsTYPE_REDIRECT
- Explicit redirect response in status declarations
// goframe:http_route path=/login method=POST response=200:LoginResponse response=401:Error
func Login() http.HandlerFunc {
// 401 responses automatically handled as errors
}
Request Structure
The generator automatically organizes request fields based on struct tags:
type CreateUserRequest struct {
// Path parameters
ID int `path:"id"`
// Query parameters
Filter string `query:"filter" optional:"true"`
Page int `query:"page" optional:"true"`
// Headers
Authorization string `header:"Authorization"`
// Cookies
SessionID string `cookie:"session_id"`
// JSON body
Name string `json:"name"`
Email string `json:"email"`
// Form data / File uploads
Avatar *multipart.FileHeader `file:"avatar"`
Bio string `form:"bio"`
}
Generated TypeScript Interface
The above Go struct generates this TypeScript interface:
interface CreateUser {
pathParams: {
id: number;
};
searchParams: {
filter?: string;
page?: number;
};
headers: {
Authorization: string;
};
cookies: {
session_id: string;
};
body: {
json: {
name: string;
email: string;
}
} | {
formData: {
avatar: File;
bio: string;
}
};
}
Generated Client Structure
Zod Schemas
Validation schemas for all request and response types:
export const createUserRequestSchema = z.object({
pathParams: z.object({
id: z.number()
}),
body: z.object({
json: z.object({
name: z.string(),
email: z.string()
})
})
}).passthrough();
TypeScript Interfaces
Type-safe interfaces matching the schemas:
export interface CreateUserRequest {
pathParams: {
id: number;
};
body: {
json: {
name: string;
email: string;
};
};
}
export interface CreateUserResponse {
id: number;
name: string;
email: string;
}
Client Functions
Functions are organized by handler namespace (based on struct name):
export namespace UserClient {
export async function createUser(
fetcher: Fetcher,
request: CreateUserRequest
): Promise<{data: CreateUserResponse, status: number, headers: Headers}> {
// Implementation with validation and HTTP call
}
export async function getUser(
fetcher: Fetcher,
request: GetUser
): Promise<{data: GetUserResponse, status: number, headers: Headers}> {
// Another route function
}
}
Fetcher Implementation
Implement the Fetcher
interface to connect with your HTTP client:
import { Fetcher, FetcherOptions } from './api';
export const createFetcher = (baseUrl: string, withToken: string | null = null): Fetcher => {
return async (options: FetcherOptions = { path: '' }) : Promise<{ data: Res; status: number; headers: Headers }> => {
const { searchParams, cookies, path, ...fetchOptions } = options;
const url = new URL(path, baseUrl);
if (searchParams) {
Object.entries(searchParams).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
});
}
let cookieHeader = '';
if (cookies) {
cookieHeader = Object.entries(cookies)
.map(([key, value]) => `${key}=${value}`)
.join('; ');
}
const headers = new Headers(fetchOptions.headers);
if (cookieHeader) {
headers.set('Cookie', cookieHeader);
}
if (withToken) {
headers.set('Authorization', `${withToken}`);
}
const fetcherOptions: RequestInit = {
...fetchOptions,
headers,
method: fetchOptions.method || 'GET',
body: fetchOptions.body,
};
const response = await fetch(url.toString(), fetcherOptions);
return {
data: response,
headers: response.headers as Headers,
status: response.status,
}
}
};
Using the Client
// Root package handler usage
try {
const result = await UserClient.createUser(fetcher, {
pathParams: { id: 123 },
body: {
json: {
name: "John Doe",
email: "john@example.com"
}
}
});
console.log(result.data); // Typed as CreateUserResponse
console.log(result.status); // HTTP status code
console.log(result.headers); // Response headers
} catch (error) {
if (error instanceof ErrorResponse) {
console.error('API Error:', error.code, error.message);
console.error('Status:', error.statusCode);
console.error('Metadata:', error.metadata);
} else if (error instanceof RequestParseError) {
console.error('Request validation failed:', error.message);
} else if (error instanceof ResponseParseError) {
console.error('Response validation failed:', error.message);
} else if (error instanceof FetchError) {
console.error('Network error:', error.message);
}
}
// Subpackage handler usage
const dashboardStats = await DashboardStatsClient.getStats(fetcher, {
pathParams: { period: "monthly" },
searchParams: { include: "charts" }
});
// Admin reports from nested package
const reports = await AdminReportsReportsClient.generateReport(fetcher, {
body: {
json: {
type: "financial",
period: "2024-Q1"
}
}
});
Error Handling
The generated client includes comprehensive error types:
ErrorResponse
API errors with structured error data (httpx.Error):
export class ErrorResponse extends Error {
code: string;
metadata?: Record<string, any>;
statusCode?: number;
}
FetchError
Network or fetch-level failures:
export class FetchError extends Error {
origin: Error;
}
RequestParseError
Request validation failures:
export class RequestParseError extends Error {
origin: Error; // Original Zod validation error
}
ResponseParseError
Response validation failures:
export class ResponseParseError extends Error {
origin: Error; // Original Zod validation error
}
Supported Go Types
The generator handles comprehensive Go type mapping:
Go Type | TypeScript Type | Zod Schema |
---|---|---|
string | string | z.string() |
int , int64 , float64 | number | z.number() |
bool | boolean | z.boolean() |
time.Time | Date | dateSchema |
time.Duration | Duration | durationSchema |
*multipart.FileHeader | File | z.instanceof(File) |
[]T | Array<T> | z.array(T) |
map[K]V | Record<K,V> | z.record(K, V) |
interface{} | any | z.any() |
Custom structs | Generated interfaces | Generated schemas |
Enums | Const enums | z.union([z.literal(...)]) |
Special Types
Duration Class:
export class Duration {
nanoseconds(): number;
milliseconds(): number;
seconds(): number;
minutes(): number;
hours(): number;
add(other: Duration): Duration;
subtract(other: Duration): Duration;
toString(): string;
static seconds(n: number): Duration;
static minutes(n: number): Duration;
// ... more factory methods
}
Enum Support:
type Status string
const (
StatusPending Status = "pending"
StatusActive Status = "active"
StatusInactive Status = "inactive"
)
Generates:
export const StatusEnum = {
PENDING: 'pending',
ACTIVE: 'active',
INACTIVE: 'inactive',
} as const;
export type StatusEnum = ValueOf<typeof StatusEnum>;
export const statusEnumSchema = z.union([
z.literal('pending'),
z.literal('active'),
z.literal('inactive')
]);
Anonymous Structs
Anonymous structs are automatically handled with generated type names:
type UserRequest struct {
Profile struct {
Name string `json:"name"`
Age int `json:"age"`
} `json:"profile"`
}
Generates a UserRequestProfile
interface and schema automatically.
Advanced Examples
Hierarchical Package Structure Example
internal/
├── v1handler/ # Root package
│ ├── router.go
│ ├── registry.go
│ ├── handler_user.go # → UserClient namespace
│ ├── admin/ # Subpackage
│ │ └── handler_admin.go # → AdminAdminClient namespace
│ └── dashboard/ # Subpackage
│ ├── handler_stats.go # → DashboardStatsClient namespace
│ └── reports/ # Nested subpackage
│ └── handler_reports.go # → DashboardReportsReportsClient namespace
└── v2handler/ # Another root package (generates separate file)
├── router.go
├── registry.go
└── handler_user.go # → UserClient namespace (in separate file)
Complex Route with Multiple Features
// goframe:http_route path=/api/v1/users/{id}/posts method=[GET, POST] name=UserPosts required_header=Authorization response=200:PostListResponse response=201:PostCreatedResponse response=400:ValidationError response=401:Error response=403:Error
func (h *UserHandler) UserPosts() http.HandlerFunc {
// Complex route with multiple methods, statuses, and requirements
}
File Upload with Form Data
type UploadAvatarRequest struct {
UserID int `path:"user_id"`
Avatar *multipart.FileHeader `file:"avatar"`
Alt string `form:"alt_text"`
}
// goframe:http_route path=/users/{user_id}/avatar method=POST
func UploadAvatar() http.HandlerFunc {
// File upload handler
}
Generated client usage:
await UserClient.uploadAvatar(fetcher, {
pathParams: { user_id: 123 },
body: {
formData: {
avatar: file, // File object from input
alt_text: "Profile picture"
}
}
});
This generator provides complete type safety from Go backend to TypeScript frontend, with automatic validation and comprehensive error handling.
Package Discovery and File Generation
The generator automatically discovers handler packages using these rules:
- Root Package Detection: Packages containing both
router.go
andregistry.go
files - Subpackage Discovery: Recursively finds all subfolders with Go handler files
- Separate File Generation: Each root package generates its own TypeScript client file
- Import Organization: Handlers from subpackages are properly namespaced and imported
Multi-Package Generation
# Generate clients for all discovered root packages
bin/goframe g client --file ./web/v1-api.ts --package internal/v1handler
bin/goframe g client --file ./web/v2-api.ts --package internal/v2handler
bin/goframe g client --file ./web/admin-api.ts --package internal/adminhandler
Each generated file contains:
- All handlers from the root package and its subfolders
- Properly namespaced client functions
- Type-safe interfaces and schemas
- Comprehensive error handling
Why no OpenAPI?
The tsclient
generator is designed to provide a more tailored and type-safe client generation experience compared to OpenAPI.
It focuses on leveraging Go’s type system and Zod for runtime validation, ensuring that the generated TypeScript clients are tightly coupled with the Go backend’s types and hierarchical package structure.
The hierarchical package support allows for better code organization and separation of concerns, making it ideal for large applications with multiple API versions or domains.
I will deliver an openapi generator in the future, but for now, the tsclient
generator is optimized for Go developers who want a seamless integration between their Go HTTP handlers and TypeScript clients without the overhead of OpenAPI specifications and poor typescript codegen.