Skip to Content
HTTPTypeScript Client

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 under internal. 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

  1. Root Package Handlers: Use handler name directly

    // internal/v1handler/handler_user.go → UserHandler export namespace UserClient { export async function getUsers(request: GetUsersRequest){} }
  2. 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){} }
  3. Multiple Root Packages: Separate files generated

    // internal/v1handler generates one file // internal/v2handler generates another file
  4. 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 codes
  • Redirect - Automatically maps to 3xx status codes
  • TYPE_ERROR - Explicit error response in status declarations
  • TYPE_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 TypeTypeScript TypeZod Schema
stringstringz.string()
int, int64, float64numberz.number()
boolbooleanz.boolean()
time.TimeDatedateSchema
time.DurationDurationdurationSchema
*multipart.FileHeaderFilez.instanceof(File)
[]TArray<T>z.array(T)
map[K]VRecord<K,V>z.record(K, V)
interface{}anyz.any()
Custom structsGenerated interfacesGenerated schemas
EnumsConst enumsz.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:

  1. Root Package Detection: Packages containing both router.go and registry.go files
  2. Subpackage Discovery: Recursively finds all subfolders with Go handler files
  3. Separate File Generation: Each root package generates its own TypeScript client file
  4. 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.

Last updated on