Skip to content

HTTP Package Usage (@grest-ts/http)

How to use the HTTP package for building type-safe HTTP and WebSocket APIs.

HTTP API Definition

Basic API Structure

typescript
// MyApi.ts
import { GGRpc, httpSchema } from "@grest-ts/http"
import { GGContractClass, IsArray, IsObject, IsString, IsBoolean, IsUint, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR } from "@grest-ts/schema"

// ---------------------------------------------------------
// Type Schemas
// ---------------------------------------------------------

export const IsItemId = IsString.brand("ItemId")
export type tItemId = typeof IsItemId.infer

export const IsItem = IsObject({
    id: IsItemId,
    title: IsString,
    description: IsString.orUndefined,
    done: IsBoolean,
    createdAt: IsUint,
    updatedAt: IsUint
})
export type Item = typeof IsItem.infer

export const IsCreateItemRequest = IsObject({
    title: IsString.nonEmpty,
    description: IsString.orUndefined
})
export type CreateItemRequest = typeof IsCreateItemRequest.infer

export const IsUpdateItemRequest = IsObject({
    id: IsItemId,
    title: IsString.orUndefined,
    description: IsString.orUndefined
})
export type UpdateItemRequest = typeof IsUpdateItemRequest.infer

export const IsItemIdParam = IsObject({
    id: IsItemId
})

// ---------------------------------------------------------
// Contract & API
// ---------------------------------------------------------

export const MyApiContract = new GGContractClass("MyApi", {
    list: {
        success: IsArray(IsItem),
        errors: [SERVER_ERROR]
    },
    get: {
        input: IsItemIdParam,
        success: IsItem,
        errors: [NOT_FOUND, SERVER_ERROR]
    },
    create: {
        input: IsCreateItemRequest,
        success: IsItem,
        errors: [VALIDATION_ERROR, SERVER_ERROR]
    },
    update: {
        input: IsUpdateItemRequest,
        success: IsItem,
        errors: [NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR]
    },
    delete: {
        input: IsItemIdParam,
        errors: [NOT_FOUND, SERVER_ERROR]
    }
})

export const MyApi = httpSchema(MyApiContract)
    .pathPrefix("api/items")
    .routes({
        list: GGRpc.GET("list"),
        get: GGRpc.GET("get/:id"),
        create: GGRpc.POST("create"),
        update: GGRpc.PUT("update"),
        delete: GGRpc.DELETE("delete/:id")
    })

HTTP Methods

typescript
GGRpc.GET("path")      // GET request
GGRpc.POST("path")     // POST request
GGRpc.PUT("path")      // PUT request
GGRpc.DELETE("path")   // DELETE request

Path Parameters

Use :paramName in paths - parameters are matched by position:

typescript
export const MyApiContract = new GGContractClass("MyApi", {
    getUser: {
        input: IsObject({ userId: IsUserId }),
        success: IsUser,
        errors: [NOT_FOUND, SERVER_ERROR]
    },
    getUserPost: {
        input: IsObject({ userId: IsUserId, postId: IsPostId }),
        success: IsPost,
        errors: [NOT_FOUND, SERVER_ERROR]
    }
})

export const MyApi = httpSchema(MyApiContract)
    .pathPrefix("api")
    .routes({
        getUser: GGRpc.GET("users/:userId"),
        getUserPost: GGRpc.GET("users/:userId/posts/:postId")
    })

Query Parameters

For GET/DELETE, object parameters become query strings:

typescript
export const MyApiContract = new GGContractClass("MyApi", {
    search: {
        input: IsObject({
            term: IsString,
            page: IsUint.orUndefined,
            limit: IsUint.orUndefined
        }),
        success: IsSearchResults,
        errors: [SERVER_ERROR]
    }
})

// Client usage: client.search({ term: "foo", page: 1 })
// Results in: GET /api/search?term=foo&page=1

Request Body

For POST/PUT, the input becomes the JSON body:

typescript
export const MyApiContract = new GGContractClass("MyApi", {
    create: {
        input: IsCreateRequest,
        success: IsItem,
        errors: [VALIDATION_ERROR, SERVER_ERROR]
    },
    update: {
        input: IsUpdateRequest,
        success: IsItem,
        errors: [VALIDATION_ERROR, SERVER_ERROR]
    }
})

Authentication & Context

typescript
// auth/UserAuth.ts
import { GGContextKey } from "@grest-ts/context"
import { IsObject, IsString } from "@grest-ts/schema"

export const IsUserAuthToken = IsString.brand("UserAuthToken")
export type tUserAuthToken = typeof IsUserAuthToken.infer

export const IsUserId = IsString.brand("UserId")
export type tUserId = typeof IsUserId.infer

export const IsUser = IsObject({
    id: IsUserId,
    username: IsString,
    email: IsString
})
export type User = typeof IsUser.infer

// Define the context value schema
const IsUserAuthContext = IsObject({
    token: IsUserAuthToken
})
export type UserAuthContext = typeof IsUserAuthContext.infer

// Define the header schema
const HEADER_AUTHORIZATION = "authorization"
const HeaderType = IsObject({
    [HEADER_AUTHORIZATION]: IsString.orUndefined
})

// Create context key with codec
export const GG_USER_AUTH = new GGContextKey<UserAuthContext>("user_auth", IsUserAuthContext)
GG_USER_AUTH.addCodec("http", HeaderType.codecTo(IsUserAuthContext, {
    encode: (headers) => {
        const authHeader = headers[HEADER_AUTHORIZATION]
        if (!authHeader || !authHeader.startsWith('Bearer ')) {
            return { token: undefined as any }  // Will fail validation if required
        }
        return { token: authHeader.substring(7) as tUserAuthToken }
    },
    decode: (value) => {
        return { [HEADER_AUTHORIZATION]: value.token ? `Bearer ${value.token}` : undefined }
    }
}))

Using Middleware (For Complex Logic)

typescript
// middleware/ClientInfoMiddleware.ts
import { GGHttpRequest, GGHttpTransportMiddleware } from "@grest-ts/http"
import { GGContextKey } from "@grest-ts/context"
import { IsObject, IsString, IsLiteral } from "@grest-ts/schema"

export interface ClientInfo {
    version: string
    platform: 'web' | 'ios' | 'android'
}

export const GG_CLIENT_INFO = new GGContextKey<ClientInfo>('clientInfo', IsObject({
    version: IsString,
    platform: IsLiteral("web", "ios", "android")
}))

export const ClientInfoMiddleware: GGHttpTransportMiddleware = {
    updateRequest(req: GGHttpRequest): void {
        const info = GG_CLIENT_INFO.get()
        if (info) {
            req.headers['x-client-version'] = info.version
            req.headers['x-client-platform'] = info.platform
        }
    },
    parseRequest(req: GGHttpRequest): void {
        GG_CLIENT_INFO.set({
            version: req.headers['x-client-version'] ?? 'unknown',
            platform: (req.headers['x-client-platform'] ?? 'web') as ClientInfo['platform']
        })
    }
}

Adding Auth/Context to API

typescript
import { GG_USER_AUTH } from "./auth/UserAuth"
import { ClientInfoMiddleware } from "./middleware/ClientInfoMiddleware"
import { GG_INTL_LOCALE } from "@grest-ts/intl"

export const MyApiContract = new GGContractClass("MyApi", {
    list: {
        success: IsArray(IsItem),
        errors: [NOT_AUTHORIZED, SERVER_ERROR]
    },
    create: {
        input: IsCreateRequest,
        success: IsItem,
        errors: [NOT_AUTHORIZED, VALIDATION_ERROR, SERVER_ERROR]
    }
})

// Chain multiple context providers
export const MyApi = httpSchema(MyApiContract)
    .pathPrefix("api/items")
    .useHeader(GG_INTL_LOCALE)        // Use codec from context key
    .useHeader(GG_USER_AUTH)          // Use codec from context key
    .use(ClientInfoMiddleware)        // Use middleware object
    .routes({
        list: GGRpc.GET("list"),
        create: GGRpc.POST("create")
    })

Public API (No Auth)

typescript
export const PublicApiContract = new GGContractClass("PublicApi", {
    status: {
        success: IsStatusResponse,
        errors: [SERVER_ERROR]
    },
    login: {
        input: IsLoginRequest,
        success: IsLoginResponse,
        errors: [VALIDATION_ERROR, SERVER_ERROR]
    }
})

export const PublicApi = httpSchema(PublicApiContract)
    .pathPrefix("pub")
    .routes({
        status: GGRpc.GET("status"),
        login: GGRpc.POST("login")
    })

Error Types

Declaring Errors in Contract

typescript
import { GGContractClass, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR, BAD_REQUEST, ERROR } from "@grest-ts/schema"

// Custom error type
const INVALID_CREDENTIALS = ERROR.define("INVALID_CREDENTIALS", 400)

export const MyApiContract = new GGContractClass("MyApi", {
    get: {
        input: IsItemIdParam,
        success: IsItem,
        errors: [NOT_FOUND, SERVER_ERROR]
    },
    update: {
        input: IsUpdateRequest,
        success: IsItem,
        errors: [NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR]
    },
    login: {
        input: IsLoginRequest,
        success: IsLoginResponse,
        errors: [INVALID_CREDENTIALS, VALIDATION_ERROR, SERVER_ERROR]
    }
})

Throwing Errors in Service

typescript
import { NOT_FOUND, FORBIDDEN } from "@grest-ts/schema"

export class MyService {
    async get({ id }: { id: tItemId }): Promise<Item> {
        const item = await this.findItem(id)
        if (!item) throw new NOT_FOUND()
        return item
    }

    async update(request: UpdateRequest): Promise<Item> {
        const item = await this.findItem(request.id)
        if (!item) throw new NOT_FOUND()

        const user = GG_USER_AUTH.get()
        if (item.ownerId !== user.id) throw new FORBIDDEN()

        return this.updateItem(item, request)
    }
}

HTTP Server Setup

Using GGHttp (Fluent API)

typescript
import { GGHttp, GGHttpServer } from "@grest-ts/http"

protected compose(): void {
    const httpServer = new GGHttpServer()

    new GGHttp(httpServer)
        .http(PublicApi, publicService)
        .http(StatusApi, {
            status: async () => ({ status: true })
        })

    new GGHttp(httpServer)
        .use(new UserContextMiddleware(userService))
        .http(MyApi, myService)
        .http(UserAuthApi, userService)
}

Using GGHttpSchema.register (Direct)

typescript
import { GGHttpServer } from "@grest-ts/http"

protected compose(): void {
    const httpServer = new GGHttpServer()
    MyApi.register(myService, { http: httpServer })
    OtherApi.register(otherService, { http: httpServer })
}

Multiple HTTP Servers

typescript
protected compose(): void {
    // Main public server
    const publicServer = new GGHttpServer()
    new GGHttp(publicServer)
        .http(PublicApi, publicService)

    // Internal server on different port
    const internalServer = new GGHttpServer({ port: 9090 })
    new GGHttp(internalServer)
        .http(InternalApi, internalService)
}

HTTP Client

Creating Clients

typescript
import { GGHttpClientConfig } from "@grest-ts/http"

// With explicit URL
const client = MyApi.createClient({ url: "http://localhost:3000" })

// Browser same-origin (use empty string)
const client = MyApi.createClient({ url: "" })

// With options
const client = MyApi.createClient({ url: "http://localhost:3000", timeout: 30000 })

// Without URL (uses service discovery)
const client = MyApi.createClient()

Making Requests

typescript
// Simple request (no input)
const items = await client.list()

// With path parameter
const item = await client.get({ id: "item-123" })

// With query parameters (GET request)
const results = await client.search({ term: "foo", page: 1 })

// With body (POST request)
const newItem = await client.create({ title: "New Item" })

Handling Results

typescript
// Direct (throws on error)
const item = await client.get({ id: "item-123" })

// Using .asResult() for safe error handling
const result = await client.get({ id: "item-123" }).asResult()
if (result.success) {
    console.log("Item:", result.data)
} else {
    console.log("Error:", result.type)  // "NOT_FOUND", etc.
}

// Using .orDefault() for fallback values
const item = await client.get({ id: "item-123" }).orDefault(() => defaultItem)

// Using .or() for error recovery
const item = await client.get({ id: "item-123" }).or((error) => {
    if (error.type === "NOT_FOUND") return defaultItem
    throw error
})

// Using .map() to transform the result
const title = await client.get({ id: "item-123" }).map(item => item.title)

WebSocket APIs

Defining WebSocket API

typescript
import { defineSocketContract, webSocketSchema } from "@grest-ts/websocket"
import { IsObject, IsString, IsBoolean, SERVER_ERROR, VALIDATION_ERROR } from "@grest-ts/schema"

// Message schemas
export const IsItemMarkedEvent = IsObject({
    item: IsItem,
    markedBy: IsString
})
export type ItemMarkedEvent = typeof IsItemMarkedEvent.infer

export const IsUpdateItemRequest = IsObject({
    item: IsItem,
    reason: IsString.orUndefined
})

export const IsUpdateItemResponse = IsObject({
    success: IsBoolean,
    message: IsString
})

// Contract definition
export const NotificationApiContract = defineSocketContract("NotificationApi", {
    clientToServer: {
        updateItem: {
            input: IsUpdateItemRequest,
            success: IsUpdateItemResponse,
            errors: [VALIDATION_ERROR, SERVER_ERROR]
        },
        ping: {}
    },
    serverToClient: {
        itemMarked: {
            input: IsItemMarkedEvent
        },
        areYouThere: {
            success: IsBoolean,
            errors: [SERVER_ERROR]
        }
    }
})

export const NotificationApi = webSocketSchema(NotificationApiContract)
    .path("ws/notifications")
    .use(AuthMiddleware)
    .done()

WebSocket Server Handler

typescript
import { WebSocketIncoming, WebSocketOutgoing } from "@grest-ts/websocket"

export class NotificationService {
    private connections = new Map<string, Set<WebSocketOutgoing<any>>>()

    handleConnection = (incoming: WebSocketIncoming<any>, outgoing: WebSocketOutgoing<any>): void => {
        const user = GG_USER_AUTH.get()

        // Track connection
        if (!this.connections.has(user.id)) {
            this.connections.set(user.id, new Set())
        }
        this.connections.get(user.id)!.add(outgoing)

        // Handle incoming messages
        incoming.on({
            updateItem: async (request) => {
                // Process update
                return { success: true, message: "Updated" }
            },
            ping: async () => {
                // Handle ping
            }
        })

        // Handle disconnect
        outgoing.onClose(() => {
            this.connections.get(user.id)?.delete(outgoing)
        })
    }

    // Broadcast to user
    notifyUser(userId: string, event: ItemMarkedEvent): void {
        const userConnections = this.connections.get(userId)
        userConnections?.forEach(conn => conn.itemMarked(event))
    }
}

WebSocket in Runtime

typescript
import { GGHttpServer } from "@grest-ts/http"

protected compose(): void {
    const httpServer = new GGHttpServer()

    new GGHttp(httpServer)
        .use(new UserContextMiddleware(userService))
        .http(MyApi, myService)

    // Register WebSocket on the same HTTP server
    NotificationApi.register(notificationService.handleConnection, { http: httpServer })
}

WebSocket Client

typescript
import { GGSocketPool } from "@grest-ts/websocket"

// Connect via socket pool (reuses connections for same URL + headers)
const socket = await GGSocketPool.getOrConnect({
    domain: "ws://localhost:3000",
    path: "/ws/notifications",
    middlewares: NotificationApi.middlewares
})

// Send messages via the socket
const response = await socket.send("NotificationApi.updateItem", { item, reason: "Updated via UI" }, true)

// Handle disconnect
socket.onClose(() => {
    console.log("Disconnected")
})

// Close connection
socket.close()

// Close all pooled connections
await GGSocketPool.closeAll()