npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@grest-ts/http

v0.0.20

Published

HTTP server and client library for Node.js and browser

Downloads

1,629

Readme

Part of the grest-ts framework. Documentation | All packages

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

// 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

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:

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:

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:

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

Using Codec (Recommended for Header-Based Auth)

// 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)

// 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

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)

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

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

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)

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)

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

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

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

// 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

// 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

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

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

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

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()