Skip to content

grest-tsContract-First Testable TypeScript Services

One contract. Typed server, typed tests, typed client. Zero magic.

grest-ts

See It In Action

Define once, implement, test — three files and you have a fully typed API.

1

Define the Contract

Single source of truth — Typia-speed validation, no compiler plugin, no build step.

ts
// api/src/api/ItemApi.ts

// Standard schema definitions.
export const IsItem = IsObject({
    id: IsNumber,
    title: IsString
})

export const IsCreateItemRequest = IsObject({
    title: IsString
})

// Define custom errors with typed data.
const OUT_OF_STOCK = GGerror.define("OUT_OF_STOCK", IsObject({
    amountLeft: IsNumber
}));

// Define your contract - think about those like a function signatures.
const ItemApiContract = new GGContractClass("ItemApi", {
    list: {
        success: IsArray(IsItem),
        errors: [SERVER_ERROR, OUT_OF_STOCK] // We say what errors our contract can return.
    },
    create: {
        input: IsCreateItemRequest,
        success: IsItem,
        errors: [VALIDATION_ERROR, SERVER_ERROR]
    }
})

export const ItemApi = httpSchema(ItemApiContract)
    .pathPrefix("api/items")
    .routes({
        list: GGRpc.GET("list"),
        create: GGRpc.POST("create")
    })
2

Wire Up and Run

One line to bind your contract. Your Runtime is the bootstrap — all wiring visible in one place.

typescript
// server/src/AppRuntime.ts
export class AppRuntime extends GGRuntime {
    public static readonly NAME = "app"

    protected compose(): void {
        new GGHttp().http(ItemApi, new ItemApiImpl())
    }
}

// Simple implementation examples
type ItemApiContract = GGContractImplementation<typeof ItemApiContract.methods>;
export class ItemApiImpl implements ItemApiContract {

    private readonly geocoder = new GeocodingService();

    public list = async (): Promise<Item[]> => {
        // ...
    }
    public create = async (input: typeof IsCreateItemRequest.infer): Promise<Item> => {
        // ... Also use geocoder here ...
        throw OUT_OF_STOCK({amountLeft: 10}) // throw errors.
    }
}

@mockable // This enables mocking/syping in tests for internal classes.
export class GeocodingService {
    async resolve(address: string): Promise<LatLng> {
        return await this.client.geocode(address)
    }
}
bash
tsx src/AppRuntime.ts    # That's it. Service is running. 
# Launch more to get load balanced multi-instance local setup...
3

Test Everything

Real workers, real ports, per-request mocks. Each test gets its own isolated runtime.

typescript
// server/test/item.test.ts
describe("Item API", () => {
    GGTest.startWorker([AppRuntime, AppRuntime]) // Start as many as you want, also different services etc. 
    // Yes, it is real worker! Can also launch startInline for fastest test run speed.
    // Yes, you can get full coverage including integration tests.
    // You can mock outbound service calls. If outbound service exists, calls go through.
    // No DI - your Runtime is the bootstrap! No more duplicating whole wiring in tests.

    const myApis = new TestContext("Items")
        .apis({
            item: ItemApi
            // Add API-s you are going to call in the tests. 
            // Can be many, different services/runtimes etc. Doesn't matter.
        })

    test("create and list items", async () => {

        // Call your API-s as you normally would in clients.
        await myApis.item.create({title: "Buy groceries"})
            .toMatchObject({id: 1, title: "Buy groceries"})

        // Anything vitest supports, you can still do - even snapshots.
        const result = await myApis.item.list()
        expect(result).toMatchSnapshot()

        // Can even mock random internal classes running within your service.
        await myApis.item.create({title: "Visit Times Square"})
            .with(
                mockOf(GeocodingService).resolve // Mock applies only during this request, nicely scoped!
                    .toEqual({address: "Times Square, NYC"})
                    .andReturn({lat: 40.758, lng: -73.985})
            )
            .toMatchObject({title: "Visit Times Square", lat: 40.758})
    })
})
bash
vitest    # Each test suite gets its own runtime with isolated ports.
4

Get started

Copy the starter service to get going quickly.

bash
# Start with a simple starter template
npm create @grest-ts/starter my-app

# Terminal 1 — server
cd server && npm run dev

# Terminal 2 — client
cd client && npm run dev
5

Get building

Everything is wired up — API contract, server handler, integration test, and a client that calls the API. Build on it.
Even your AI buddy is going to enjoy it!