JavaScript Client

Whilst you can use any of the multitude of Ajax libraries to consume ServiceStack's pure JSON REST APIs, leveraging the integrated TypeScript support still offers the best development UX for calling ServiceStack's JSON APIs in JavaScript where you can use the TypeScript JsonServiceClient with TypeScript Add ServiceStack Reference DTO's to get the same productive end-to-end Typed APIs available in ServiceStack's Typed .NET Clients, e.g:

let client = new JsonServiceClient(baseUrl)

client.get(new Hello({ Name: 'World' }))
  .then(r => console.log(r.Result))

Instant Typed JavaScript DTOs 🚀

Ultimately the key to maximizing productivity is avoiding things that interrupt your dev workflow. Since we use JsonServiceClient for all API requests one area that still interrupts us is regenerating TypeScript dtos.ts after adding new APIs, as it was the only way to generate typed DTOs for use in the generic JsonServiceClient in both Vanilla JS and TypeScript Apps.

For Vanilla JS Apps typically this means running the x dotnet tool to update dtos.ts, then using TypeScript to compile it to JavaScript:

$ x ts && tsc dtos.ts 

While not a great hindrance, it can frequently interrupt our workflow when developing new APIs.

Although thanks to the native JavaScript Language support this can be avoided by referencing Typed JavaScript DTOs directly from /types/js

Since importing JavaScript doesn't require any tooling or build steps, it greatly simplifies calling ServiceStack APIs from Vanilla JS Apps which all our .NET 6 Empty Project Templates take advantage of for its now optimal friction-less dev model.

Using /types/js has the same behavior as using dtos.js generated from $ tsc dtos.ts whose outputs are identical, i.e. both containing your API DTOs generated in CommonJS format. It's feasible to simulate the TypeScript compiler's output in this instance as ServiceStack only needs to generate DTO Types and Enums to enable its end-to-end API, and not any other of TypeScript's vast featureset.

Using JavaScript Typed DTOs in Web Apps

To get started quickly you can use the init mix gist to create an empty .NET project:

mkdir ProjectName && cd ProjectName

x mix init

That uses the built-in @servicestack/client library's JsonServiceClient in a dependency-free Web Page:

To make typed API Requests from web pages, you need only include /js/require.js containing a simple require() to load CommonJS libraries, /js/servicestack-client.js (production build of @servicestack/client) and /types/js containing your APIs typed JS DTOs - all built-in ServiceStack.

After which you'll have access to full feature-set of the generic JsonServiceClient with your APIs Typed Request DTOs, e.g:

<script src="/js/require.js"></script>
<script src="/js/servicestack-client.js"></script>
<script src="/types/js"></script>

Which utilizes the JavaScript Add ServiceStack Reference /types/js to instantly generate JavaScript Types for all your APIs DTOs which can immediately be used with the TypeScript JsonServiceClient to make Typed API requests:

<script>
var { JsonServiceClient, Hello } = exports

var client = new JsonServiceClient();
function callHello(name) {
    client.get(new Hello({ name }))
        .then(function(r) {
            document.getElementById('result').innerHTML = r.result;
        });
}
</script>

JsonServiceClient

API method

The api returns a typed ApiResult<Response> Value Result that encapsulates either a Typed Response or a structured API Error populated in ResponseStatus allowing you to handle API responses programmatically without try/catch handling:

const api = client.api(new Hello({ name }))
if (api.failed) {
    console.log(`Greeting failed! ${e.errorCode}: ${e.errorMessage}`);
    return;
}

console.log(`API Says: ${api.response.result}`) //api.succeeded

Simplified API Handling

Being able to treat errors as values greatly increases the ability to programmatically handle and genericise api handling and greatly simplifies functionality needing to handle both successful and error responses like binding to UI components.

An example of this is below where we're able to concurrently fire off multiple unrelated async requests in parallel, wait for them all to complete, print out the ones that have succeeded or failed then access their strong typed responses:

import { JsonServiceClient } from "@servicestack/client"

let requests:ApiRequest[] = [
    new AppOverview(),            // GET => AppOverviewResponse
    new DeleteTechnology(),       // DELETE => IReturnVoid (requires auth) 
    new GetAllTechnologies(),     // GET => GetAllTechnologiesResponse
    new GetAllTechnologyStacks(), // GET => GetAllTechnologyStacksResponse
]

let results = await Promise.all(requests.map(async (request) =>
    ({ request, api:await client.api(request) as ApiResponse}) ))

let failed = results.filter(x => x.api.failed)
console.log(`${failed.length} failed:`)
failed.forEach(x =>
    console.log(`    ${x.request.getTypeName()} Request Failed: ${failed.map(x => x.api.errorMessage)}`))

let succeeded = results.filter(x => x.api.succeeded)
console.log(`\n${succeeded.length} succeeded: ${succeeded.map(x => x.request.getTypeName()).join(', ')}`)

let r = succeeded.find(x => x.request.getTypeName() == 'AppOverview')?.api.response as AppOverviewResponse
if (r) console.log(`Top 5 Technologies: ${r.topTechnologies.slice(0,4).map(tech => tech.name).join(', ')}`)

Output:

1 failed
DeleteTechnology Request Failed: Unauthorized

3 succeeded: AppOverview, GetAllTechnologies, GetAllTechnologyStacks
Top 5 Technologies: Redis, MySQL, Amazon EC2, Nginx

Being able to treat Errors as values has dramatically reduced the effort required to accomplish the same feat if needing to handle errors with try/catch.

Ideal Typed Message-based API

The TypeScript JsonServiceClient enables the same productive, typed API development experience available in ServiceStack's other 1st-class supported client platforms.

The JsonServiceClient leverages the additional type hints ServiceStack embeds in each TypeScript Request DTO to achieve the ideal typed, message-based API - so all API requests benefit from a succinct, boilerplate-free Typed API.

Here's a quick look at what it looks like. The example below shows how to create a C# Gist in Gistlyn after adding a TypeScript ServiceStack Reference to gistlyn.com and installing the @servicestack/client npm package:

import { JsonServiceClient } from '@servicestack/client';
import { StoreGist, GithubFile } from './dtos';

const client = new JsonServiceClient("https://gistlyn.com");

const request = new StoreGist({
    files: { 
        [file.filename]: new GithubFile({
            filename: 'main.cs',
            content: 'var greeting = "Hi, from TypeScript!";' 
        }) 
    }
})

const api = client.api(request); //response:StoreGistResponse
if (api.succeeded) {
    console.log(`New C# Gist was created with id: ${r.gist}`);
    location.href = `https://gist.cafe/${r.gist}`;
} else {
    console.log("Failed to create Gist: ", e.errorMessage);
}

Where the response param is typed to StoreGistResponse DTO Type.

Sending additional arguments with Typed API Requests

Many AutoQuery Services utilize implicit conventions to query fields that aren't explicitly defined on AutoQuery Request DTOs, these can now be queried by specifying additional arguments with the typed Request DTO, e.g:

//typed to QueryResponse<TechnologyStack> 
const response = await client.get(new FindTechStacks(), { VendorName: "ServiceStack" });

Which will return TechStacks developed by ServiceStack.

Calling APIs with Custom URLs

You can call Services using relative or absolute urls, e.g:

client.get<GetTechnologyResponse>("/technology/ServiceStack")

client.get<GetTechnologyResponse>("http://techstacks.io/technology/ServiceStack")

// GET http://techstacks.io/technology?Slug=ServiceStack
client.get<GetTechnologyResponse>("/technology", { Slug: "ServiceStack" }) 

as well as POST Request DTOs to custom urls:

client.postToUrl("/custom-path", request, { Slug: "ServiceStack" });

client.putToUrl("http://example.org/custom-path", request);

Raw Data Responses

The JsonServiceClient also supports Raw Data responses like string and byte[] which also get a Typed API once declared on Request DTOs using the IReturn<T> marker:

public class ReturnString : IReturn<string> {}
public class ReturnBytes : IReturn<byte[]> {}

Which can then be accessed as normal, with their Response typed to a JavaScript string or Uint8Array for raw byte[] responses:

let str:string = await client.get(new ReturnString());

let data:Uint8Array = await client.get(new ReturnBytes());

Authenticating using Basic Auth

Basic Auth support is implemented in JsonServiceClient and follows the same API made available in the C# Service Clients where the userName/password properties can be set individually, e.g:

var client = new JsonServiceClient(baseUrl);
client.userName = user;
client.password = pass;

const response = await client.get(new SecureRequest());

Or use client.setCredentials() to have them set both together.

Authenticating using Credentials

Alternatively you can authenticate using userName/password credentials by adding a TypeScript Reference to your remote ServiceStack Instance and sending a populated Authenticate Request DTO, e.g:

const response = await client.post(new Authenticate({
    provider: "credentials", userName, password, rememberMe: true }));

This will populate the JsonServiceClient with Session Cookies which will transparently be sent on subsequent requests to make authenticated requests.

Authenticating using JWT

Use the bearerToken property to Authenticate with a ServiceStack JWT Provider using a JWT Token:

client.bearerToken = jwtToken;

Alternatively you can use a Refresh Token instead:

client.refreshToken = refreshToken;

Authenticating using an API Key

Use the bearerToken property to Authenticate with an API Key:

client.bearerToken = apiKey;

Transparently handle 401 Unauthorized Responses

If the server returns a 401 Unauthorized Response either because the client was Unauthenticated or the configured Bearer Token or API Key used had expired or was invalidated, you can use onAuthenticationRequired callback to re-configure the client before automatically retrying the original request, e.g:

client.onAuthenticationRequired = async () => {
    const authClient = new JsonServiceClient(authBaseUrl);
    authClient.userName = userName;
    authClient.password = password;
    const response = await authClient.get(new Authenticate());
    client.bearerToken = response.bearerToken;
};

//Automatically retries requests returning 401 Responses with new bearerToken
var response = await client.get(new Secured());

Automatically refresh Access Tokens

With the Refresh Token support in JWT you can use the refreshToken property to instruct the Service Client to automatically fetch new JWT Tokens behind the scenes before automatically retrying failed requests due to invalid or expired JWTs, e.g:

//Authenticate to get new Refresh Token
const authClient = new JsonServiceClient(authBaseUrl);
authClient.userName = userName;
authClient.password = password;
const authResponse = await authClient.get(new Authenticate());

//Configure client with RefreshToken
client.refreshToken = authResponse.RefreshToken;

//Call authenticated Services and clients will automatically retrieve new JWT Tokens as needed
const response = await client.get(new Secured());

Use the refreshTokenUri property when refresh tokens need to be sent to a different ServiceStack Server, e.g:

client.refreshToken = refreshToken;
client.refreshTokenUri = authBaseUrl + "/access-token";

ServerEvents Client

The TypeScript ServerEventClient is an idiomatic port of ServiceStack's C# Server Events Client in native TypeScript providing a productive client to consume ServiceStack's real-time Server Events that can be used in TypeScript Web, Node.js Server and React Native iOS and Android Mobile Apps.

const channels = ["home"];
const client = new ServerEventsClient("/", channels, {
    handlers: {
        onConnect: (sub:ServerEventConnect) => {  // Successful SSE connection
            console.log("You've connected! welcome " + sub.displayName);
        },
        onJoin: (msg:ServerEventJoin) => {        // User has joined subscribed channel
            console.log("Welcome, " + msg.displayName);
        },
        onLeave: (msg:ServerEventLeave) => {      // User has left subscribed channel
            console.log(msg.displayName + " has left the building");
        },
        onUpdate: (msg:ServerEventUpdate) => {    // User channel subscription was changed
            console.log(msg.displayName + " channels subscription were updated");
        },        
        onMessage: (msg:ServerEventMessage) => {},// Invoked for each other message
        //... Register custom handlers
        announce: (text:string) => {},            // Handle messages with simple argument
        chat: (chatMsg:ChatMessage) => {},        // Handle messages with complex type argument
        CustomMessage: (msg:CustomMessage) => {}, // Handle complex types with default selector
    },
    receivers: { 
        //... Register any receivers
        tv: {
            watch: function (id) {                // Handle 'tv.watch {url}' messages 
                var el = document.querySelector("#tv");
                if (id.indexOf('youtu.be') >= 0) {
                    var v = splitOnLast(id, '/')[1];
                    el.innerHTML = templates.youtube.replace("{id}", v);
                } else {
                    el.innerHTML = templates.generic.replace("{id}", id);
                }
                el.style.display = 'block'; 
            },
            off: function () {                    // Handle 'tv.off' messages
                var el = document.querySelector("#tv");
                el.style.display = 'none';
                el.innerHTML = '';
            }
        }
    },
    onException: (e:Error) => {},                 // Invoked on each Error
    onReconnect: (e:Error) => {}                  // Invoked after each auto-reconnect
})
.addListener("theEvent",(e:ServerEventMessage) => {}) // Add listener for pub/sub event trigger
.start();                                             // Start listening for Server Events!

When publishing a DTO Type for your Server Events message, your clients will be able to benefit from the generated DTOs in TypeScript ServiceStack References.

Rich intelli-sense support

Even pure HTML/JS Apps that don't use TypeScript or any external dependencies will still benefit from the Server generated dtos.ts and servicestack-client.d.ts definitions as Smart IDEs like Rider can make use of them to provide a rich productive development UX on both the built-in /js/servicestack-client.js library:

As well as your App's server generated DTOs:

Including their typed partial constructors:

So even simple Apps without complex bundling solutions or external dependencies can still benefit from a rich typed authoring experience without any additional build time or tooling complexity.

CDN Resources

An CDN alternative to using the @servicestack/client built into ServiceStack.dll is to reference it from unpkg.com:

If needed for IDE intelli-sense, the TypeScript definition for the @servicestack/client is available from:

The npm-free Vue and React lite Templates are some examples that makes use of the stand-alone @servicestack/client libraries.

Using TypeScript JsonServiceClient in npm projects

The /@servicestack/client follows the recommended guidance for TypeScript modules which doesn't bundle any TypeScript .ts source files, just the generated index.js and index.d.ts Type definitions which can be imported the same way in both JavaScript and TypeScript npm projects as any other module, e.g:

import { JsonServiceClient } from "@servicestack/client"

Which can then be used with the generated DTOs from your API at /types/typescript that can either be downloaded and saved to a local file e.g. dtos.ts or preferably downloaded using the x dotnet tool to download the DTOs of a remote ServiceStack API with:

dotnet tool install --global x

Then generate DTOs with:

x typescript http://yourdomain.org

For JavaScript projects that haven't configured transpilation of TypeScript, you'll need to use TypeScript to generate the dtos.js JavaScript version which can be used instead:

tsc dtos.ts

Use the --module compiler flag if needing to generate a specific module version, e.g:

tsc -m ES6 dtos.ts

The generated dtos.js can then be used with the JsonServiceClient to provide a succinct Typed API:

import { GetConfig } from './dtos';

let client = new JsonServiceClient('/');

let response = await client.get(new GetConfig());

Updating DTOs

To update your generated DTOs when your server API changes, run x typescript or its shorter x ts alias without any arguments:

x ts

Which will update to the latest version of dtos.ts. This can be easily automated with an [npm script][5], e.g:

{
  "scripts": {
    "dtos": "cd path/to/dtos && x ts && tsc -m ES6 dtos.ts",
    }
}

Which will let you update and compile the dtos with:

npm run dtos

The [TechStacks][6] (Vue/Nuxt) and [React Native Mobile App][7] (React) are examples of JavaScript-only projects using the TypeScript JsonServiceClient in this way.

Install

Built-in @servicestack/client

HTML and VanillaJS Web Apps can use the built-in UMD @servicestack/client in ServiceStack.dll to call ServiceStack Services without any external dependencies, e.g:

<script src="/js/require.js"></script>
<script src="/js/servicestack-client.js"></script>
<script src="/types/js"></script>
<script>
var { JsonServiceClient, Hello } = exports

var client = new JsonServiceClient();
function callHello(name) {
    client.get(new Hello({ name }))
        .then(function(r) {
            document.getElementById('result').innerHTML = r.result;
        });
}
</script>

Which utilizes the JavaScript Add ServiceStack Reference /types/js to instantly generate JavaScript Types for all your APIs DTOs which can immediately be used with the TypeScript JsonServiceClient to make Typed API requests.

CDN unpkg

A CDN hosted version of UMD @servicestack/client is available on unpkg.com at:

<script src="https://unpkg.com/@servicestack/client/dist/servicestack-client.min.js"></script>

Reference in npm projects

If you started with any of the SPA Project Templates @servicestack/client is already included, other TypeScript or ES6 projects can install @servicestack/client from npm with:

$ npm install @servicestack/client