Skip to main content

Typescript for Web APIs

TST, Hongkong

Typescript Setup

npm install typescript
touch HelloWorld.ts
let message: string = 'Hello World!'
console.log(message)
tsc HelloWorld.ts --watch
node ./HelloWorld.js

Hello World!

Or test the script inside an HTML page:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello World</title>
  </head>
  <body>
    <header></header>
    <main></main>
    <footer></footer>
    <script src='HelloWorld.js'></script>
    <script>
      let heading = document.createElement('h1');
      heading.textContent = message;
      document.body.appendChild(heading)
    </script>
  </body>
</html>

Typescript Basics

Typescript Data Type Annotation

Boolean

let isAvailable: boolean = true;
let isHidden: boolean = false;

console.log(isAvailable, isHidden)

Number

let decimalNumber: number = 101;
let binaryNumber: number = 0b1010;
let octalNumber: number = 0o777;
let hexdecimalNumber: number = 0x1F;

let x: number = 45
let y: number = 111

function add(x: number, y: number): number {
    return x + y;
}

let addedValues = (x: number, y: number): number => {
    return x + y;
}

console.log(decimalNumber, binaryNumber, octalNumber, hexdecimalNumber, add(x,y), addedValues)

String

let scope: string = 'World'
let message: string = `Hello ${scope}!`;
console.log(message)

Array

let places: Array<string> =['Hong Kong','Phnom Penh','Albany']
let numbers: number[] = [22,66,198]
console.log(places,numbers)

Tuple

let employee: [string, number] =['Mathew Cortège', 819034]
let numbers: number[] = [22,66,198]
console.log(places,numbers)

Enums

enum AItems {
    ItemA, //0
    ItemB, //1
    ItemC //2
}

enum BItems {
    ItemA = 1,
    ItemB = 2,
    ItemC = 3
}

let selectedAItem: string = AItem[1]
let selectedBItem: string = BItem[1]

console.log(selectedAItem,selectedBItem)

Any

let dynamicValue: any = 'A string value'
console.log(dynamicValue.length)

Void

function logger(): void {
    console.log('INFO :: Logger was called')
}

const result: void = logger()

console.log(result) // undefined

Null

let data: string | null = null;  // data is null unless assigned a string
let value: number; // value is undefined by default

console.log(data, value)

Never

function throwError(message: string): never {
    trow new Error(message);
}

function infiniteLoop(): never {
    while(true) {}
}

function checkNever(x: string | number): never {
    throw new Error('ERROR :: Unexpected value: ' + x);
}

Unknown

let input: unknown;

input = 44;
console.log(input);

input = 'Test';
console.log(input);

input = false;
console.log(input);

Type Assertion

let anyValue: any = '101';
let num: number = <number>anyValue;

console.log(num*2, typeof num); // 202 string
let anyValue: any = '303';
let num: number = anyValue as number;

console.log(num*2, typeof num); // 606 string

Type Casting

const userId = Number(post.userId)
const userName = String(post.userName)

Interfaces

interface Shape {
    calculateArea(): number;
}

class Circle implements Shape {
    constructor(private radius: number) {}

    calculateArea(): number {
        return Math.PI * Math.pow(this.radius, 2);
    }
}

const circle = new Circle(5);
console.log(circle.calculateArea())
interface TextApiResponse {
    text: string
}

...
const textData = await response.text()

const response: TextApiResponse = {
    text: textData
}

setUserState(response)
...
interface JsonApiResponse {
    data: UserDataNamespace.UserData
}

const fetchJSONDataFromServer = async() => {
    try {
        const apiResponse = await fetch(`http://localhost:8080/status?userId=${userId}`, {
            method: 'GET'
        })

        if (!apiResponse.ok) {
            throw new Error(`ERROR :: Request failed with status code: ${apiResponse.status}`)
        }

        const jsonData = await apiResponse.json()

        const response: JsonApiResponse = {
            data: jsonData
        }

        setUserStatus(response)
    } catch (error) {
        console.error('Error :: ', error)
    }
}

Namespaces

namespace StringUtilities
{
    function ToUppercase(str: string): string {
        return str.toUpperCase();
    }

    export function SubString(str: string, from: number, length: number=0): string {
        return ToUppercase(str.slice(from, length))
    }
}

let str: string = 'Hello there!';
let from: number = 1;
let len: number = 5;

console.log(StringUtilities.SubString(str, from, len)); //ELLO
namespace FlightDataNamespace {
    export interface FlightData {
        states: StateData[];
    }

    export interface StateData {
        icao24: string;
        callsign: string;
        country: string;
        longitude: number;
        latitude: number;
        altitude: number;
        trueTrack: number;
    }
}

export default FlightDataNamespace
type HistoryElement = {
    latitude: number;
    longitude: number;
    timestamp: number;
}

import { FlightData, StateData } from './flightData';
import FlightDataNamespace from './flightData';

const initialFlightData: FlightDataNamespace.FlightData = { states: [] };

Fetch API

Mock Web API

Use json-server to mock a Web API:

sudo npm install -g json-server
json-server --watch api.json --port 8080 --delay 200

api.json

{
  "users": [
    {
      "id": "1",
      "name": "Julia Ortega",
      "username": "jutega1337",
      "email": "jutega@email.com"
    },
    {
      "id": "2",
      "name": "William Wong",
      "username": "will69",
      "email": "wwill@email.com"
    }
  ]
}

Test the API using a curl request:

curl http://localhost:8080/users/1
{
  "id": "1",
  "name": "Julia Ortega",
  "username": "jutega1337",
  "email": "jutega@email.com"
}
curl 'http://localhost:8080/users?username=will69'
[
  {
    "id": "2",
    "name": "William Wong",
    "username": "will69",
    "email": "wwill@email.com"
  }
]

Fetch Request

interface UserProfile {
    id: number
    name: string
    email: string
}

const userId: number = 1
const userApi: string = 'http://localhost:8080/users'


async function fetchUserProfile(userId: number, userApi: string): Promise<UserProfile> {

    const response = await fetch(`${userApi}/${userId}/`)

    if (!response.ok) {
        throw new Error('ERROR :: Failed to fetch user profile')
    }

    const userProfile = await response.json()

    console.log('User Profile: ', userProfile)

    return userProfile
}


fetchUserProfile(userId, userApi)
    .then((userProfile) => {
        console.log('User ID: ', userProfile.id)
        console.log('Username: ', userProfile.name)
        console.log('User Email: ', userProfile.email)
    })
    .catch((error) => {
        console.error('Error :: ', error)
    })

Query Parameter

interface UserProfile {
    id: number
    name: string
    email: string
}

const userName: string = 'will69'
const userApi: string = 'http://localhost:8080/users'


async function fetchUserProfileByQuery(userName: string, userApi: string): Promise<UserProfile> {

    const response = await fetch(`${userApi}/?username=${userName}`)

    if (!response.ok) {
        throw new Error('ERROR :: Failed to fetch user profile')
    }

    const userProfiles = await response.json()
    
    return userProfiles
}


fetchUserProfileByQuery(userName, userApi)
    .then((userProfiles) => {
        console.log('User ID: ', userProfiles[0].id)
        console.log('Username: ', userProfiles[0].name)
        console.log('User Email: ', userProfiles[0].email)
    })
    .catch((error) => {
        console.error('Error :: ', error)
    })

Posting Data

interface UserProfile {
    id: number
    name: string
    email: string
}

const userData: UserProfile = {
    id: 3,
    name: 'Daemon Swenska',
    email: 'dswe@gmail.com'
}

const userApi: string = 'http://localhost:8080/users'

function createUserProfile(userData: UserProfile, userApi: string): void {

    fetch(userApi, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(userData)
    })
        .then(response => response.json())
        .then(data => {
            console.log('INFO :: User registration: ', data)
        })
        .catch(error => {
            console.error('ERROR :: User registration: ', error)
        })
}


createUserProfile(userData, userApi)

Authorization Header

Accessing an INSTAR IP camera CGI web interface can be done using token authentication. Start by retrieving a valid access token using the following CGI command:

/param.cgi?cmd=gettoken

cmd="gettoken";
token="7PZv8N63c7wYYRxBw39pnRYNRnbdaBTTaK";
validity="1706973422";
response="200";

Now use the token to create a custom request header that gives you access to the secured API:

interface GetRequest {
    cgiGetCommand: string,
    cameraIp: string,
    cameraPort: string,
    authToken: string
}

const getRequest: GetRequest = {
    cgiGetCommand: 'getmqttattr',
    cameraIp: '192.168.2.125',
    cameraPort: '80',
    authToken: '7PZv8N63c7wYYRxBw39pnRYNRnbdaBTTaK'
}

interface GetMqttApiResponse {
    mq_enable: string,
    mq_broker: string,
    mq_broker_ws: string,
    mq_broker_ws_port: string,
    mq_broker_ws_portssl: string,
    mq_broker_min_tls: string,
    mq_host: string,
    mq_port: string,
    mq_portssl: string,
    mq_ssl: string,
    mq_auth: string,
    mq_user: string,
    mq_insecure: string,
    mq_prefix: string,
    mq_lwt: string,
    mq_lwmon: string,
    mq_lwmoff: string,
    mq_clientid: string,
    mq_qos: string
}


const fetchDataWithAuthHeader = async (getRequest: GetRequest) => {
    try {
        const customHeaders = new Headers()

        customHeaders.append('Authorization', 'Bearer ' + getRequest.authToken)

        const response = await fetch(
            'http://' + getRequest.cameraIp + ':' + getRequest.cameraPort + '/' + 'param.cgi?cmd=' + getRequest.cgiGetCommand,
             {
                method: 'GET',
                headers: customHeaders
             }
        )
        if (!response.ok) {
            throw new Error(`ERROR :: Request failed with status ${response.status}`)
        }

        const textResponse: string = await response.text()
        
        const cleanedTextResponse: string = textResponse
            .replace('cmd="getmqttattr";', '{"')
            .replace('response="200";', '}')
            .replace(/=/g, '":')
            .replace(/";/g, '","')
            .replace(/\s/g, '')
            .replace(/","}/g, '"}');
            
        const jsonData: GetMqttApiResponse = JSON.parse(cleanedTextResponse)

        

        return jsonData
    }

    catch (error) {
        console.log('ERROR :: ', error.message)
        return null
    }
}

fetchDataWithAuthHeader(getRequest).then((jsonData) => {
    if (jsonData) {
        console.log(`BROKER CONFIGURATION:\n\nEnable Broker: ${jsonData.mq_enable},\nWebsocket Support: ${jsonData.mq_broker_ws},\nWebsocket Port: ${jsonData.mq_broker_ws_port},\nWebsocket Port SSL: ${jsonData.mq_broker_ws_portssl},\nTLS Version: ${jsonData.mq_broker_min_tls},\nExternal Broker IP: ${jsonData.mq_host},\nBroker Port: ${jsonData.mq_port},\nBroker Port SSL: ${jsonData.mq_portssl},\nEnable Encryption: ${jsonData.mq_ssl},\nEnable Authentication: ${jsonData.mq_auth},\nUsername: ${jsonData.mq_user},\nTLS Certificate Verification: ${jsonData.mq_insecure},\nMQTT Prefix: ${jsonData.mq_prefix},\nMQTT LWT: ${jsonData.mq_lwt},\nMQTT LWT on: ${jsonData.mq_lwmon},\nMQTT LWT off: ${jsonData.mq_lwmoff},\nMQTT Client ID: ${jsonData.mq_clientid},\nMQTT QoS Level: ${jsonData.mq_qos}`)
    } else {
        console.log('ERROR :: Fetching MQTT configuration failed.')
    }
}).catch((error) => {
    console.error('ERROR :: Authentication failed: ', error)
})

UPDATE :: Auto-fetch Auth-Token

Use dotenv to be able to read in .env environment variables:

npm install dotenv --save

Fetch the token using basic authentication and write it to an .env file:

const fs = require('fs')

const getToken = {
    cgiGetCommand: 'gettoken',
    cameraIp: '192.168.2.125',
    cameraPort: '80',
    userName: 'admin',
    userPass: 'instar'
}

const fetchAuthToken = async (getToken) => {
    try {
        const response = await fetch(
            'http://' + getToken.cameraIp + ':' + getToken.cameraPort + '/' + 'param.cgi?cmd=' + getToken.cgiGetCommand + '&user=' + getToken.userName + '&pwd=' + getToken.userPass
        )

        if (!response.ok) {
            throw new Error(`ERROR :: Request failed with status ${response.status}`)
        }

        const textResponse = await response.text()

        console.log(textResponse)

        const cleanedTextResponse = textResponse
            .replace('cmd="gettoken";', '{"')
            .replace('response="200";', '}')
            .replace(/=/g, '":')
            .replace(/";/g, '","')
            .replace(/\s/g, '')
            .replace(/","}/g, '"}');

        const jsonData = JSON.parse(cleanedTextResponse)

        console.log(`Access Token: ${jsonData.token}`)

        return jsonData
    }

    catch (error) {
        console.log('ERROR :: ', error.message)
        return null
    }
}

fetchAuthToken(getToken).then((jsonData) => {
    if (jsonData) {
        fs.writeFile('.env', 'AUTH_TOKEN=' + jsonData.token, (err) => { if (err) throw err })
    } else {
        console.log('ERROR :: Fetching auth token failed.')
    }
}).catch((error) => {
    console.error('ERROR :: Authentication failed: ', error)
})

Run the script:

node getToken.js

cmd="gettoken";
token="4rOdPAFuUlTDcvEQQLrZTEPSBDUNrlObyy";
validity="1707044442";
response="200";

Access Token: 4rOdPAFuUlTDcvEQQLrZTEPSBDUNrlObyy

The .env file will now contain the token AUTH_TOKEN=4rOdPAFuUlTDcvEQQLrZTEPSBDUNrlObyy. and the file can be used like:

require('dotenv').config()
console.log(`Database name is ${process.env.AUTH_TOKEN}`);

useEnvFileForAuth.ts

require('dotenv').config()

interface GetRequest {
    cgiGetCommand: string,
    cameraIp: string,
    cameraPort: string,
    authToken: string
}

const getMqttAttr: GetRequest = {
    cgiGetCommand: 'getmqttattr',
    cameraIp: '192.168.2.125',
    cameraPort: '80',
    authToken: process.env.AUTH_TOKEN
}

interface GetMqttApiResponse {
    mq_enable: string,
    mq_broker: string,
    mq_broker_ws: string,
    mq_broker_ws_port: string,
    mq_broker_ws_portssl: string,
    mq_broker_min_tls: string,
    mq_host: string,
    mq_port: string,
    mq_portssl: string,
    mq_ssl: string,
    mq_auth: string,
    mq_user: string,
    mq_insecure: string,
    mq_prefix: string,
    mq_lwt: string,
    mq_lwmon: string,
    mq_lwmoff: string,
    mq_clientid: string,
    mq_qos: string
}


const fetchMqttAttrWithAuthHeader = async (getRequest: GetRequest) => {
    try {
        const customHeaders = new Headers()

        customHeaders.append('Authorization', 'Bearer ' + getRequest.authToken)

        const response = await fetch(
            'http://' + getRequest.cameraIp + ':' + getRequest.cameraPort + '/' + 'param.cgi?cmd=' + getRequest.cgiGetCommand,
             {
                method: 'GET',
                headers: customHeaders
             }
        )
        if (!response.ok) {
            throw new Error(`ERROR :: Request failed with status ${response.status}`)
        }

        const textResponse: string = await response.text()
        
        const cleanedTextResponse: string = textResponse
            .replace('cmd="getmqttattr";', '{"')
            .replace('response="200";', '}')
            .replace(/=/g, '":')
            .replace(/";/g, '","')
            .replace(/\s/g, '')
            .replace(/","}/g, '"}');
            
        const jsonData: GetMqttApiResponse = JSON.parse(cleanedTextResponse)

        

        return jsonData
    }

    catch (error) {
        console.log('ERROR :: ', error.message)
        return null
    }
}

fetchMqttAttrWithAuthHeader(getRequest).then((jsonData) => {
    if (jsonData) {
        console.log(`BROKER CONFIGURATION:\n\nEnable Broker: ${jsonData.mq_enable},\nWebsocket Support: ${jsonData.mq_broker_ws},\nWebsocket Port: ${jsonData.mq_broker_ws_port},\nWebsocket Port SSL: ${jsonData.mq_broker_ws_portssl},\nTLS Version: ${jsonData.mq_broker_min_tls},\nExternal Broker IP: ${jsonData.mq_host},\nBroker Port: ${jsonData.mq_port},\nBroker Port SSL: ${jsonData.mq_portssl},\nEnable Encryption: ${jsonData.mq_ssl},\nEnable Authentication: ${jsonData.mq_auth},\nUsername: ${jsonData.mq_user},\nTLS Certificate Verification: ${jsonData.mq_insecure},\nMQTT Prefix: ${jsonData.mq_prefix},\nMQTT LWT: ${jsonData.mq_lwt},\nMQTT LWT on: ${jsonData.mq_lwmon},\nMQTT LWT off: ${jsonData.mq_lwmoff},\nMQTT Client ID: ${jsonData.mq_clientid},\nMQTT QoS Level: ${jsonData.mq_qos}`)
    } else {
        console.log('ERROR :: Fetching MQTT configuration failed.')
    }
}).catch((error) => {
    console.error('ERROR :: Authentication failed: ', error)
}) 

Compile the Javascript file and run it in Node.js:

tsc useEnvFileForAuth.ts --lib es2020,dom,es2015.collection,es2015.promise
node useEnvFileForAuth.js

Using Local Storage

Only available when executed inside a web browser localStorage allows you to cache an API response and check if the information was already retrieved before running an API request:

const username: string = 'admin'
const password: string = 'instar'
const loggedIn: boolean = false

const saveLoginCredentials = () => {
    if (typeof window !== 'undefined') {
        localStorage.setItem(`username`, username)
        localStorage.setItem(`password`, password)
    }
}

const fillLogin = (user, pass) => {
    console.log('INFO :: Active user credentials ', user, pass)
}

const handleCredentialInput = () => {
    if (typeof window !== 'undefined') {
        const savedUsername = localStorage.getItem('username')
        const savedPassword = localStorage.getItem('password')
        // If username and password exist fill out login form
        if (savedUsername && savedPassword) {
            fillLogin(savedUsername, savedPassword)
        }
    } else fillLogin(username, password)
}

const setIsLoggedIn = (bool) => {
    const loggedIn: boolean = bool
    console.log('INFO :: User logged in: ', loggedIn)
    handleCredentialInput()
}

const handleLogin = async (username: string, password: string) => {
    try {
        const response =await fetch('http://localhost:8080/logins', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ username, password })
        })
        if (response.ok) {
            if (typeof window !== 'undefined') {
                localStorage.setItem('username', username)
                localStorage.setItem('password', password)
    
                setIsLoggedIn(true)
            } else setIsLoggedIn(true)
        }
        else {
            console.error('ERROR :: Login failed!')
        }
    } catch (error) {
        console.error('ERROR :: Login error ', error)
    }
}

const checkStoredCredentials = () => {
    if (typeof window !== 'undefined') {
        const storedUsername = localStorage.getItem('username')
        const storedPassword = localStorage.getItem('password')
        // If username and password exist fill out login form
        if (storedUsername && storedPassword) {
            handleLogin(storedUsername, storedPassword)
        }
    } else handleLogin(username, password)
}


checkStoredCredentials()

Another example using the Star Wars API:

async function fetchJediMindMelt(): Promise<any> {
    // check if data has already been cached
    const cacheKey = 'forceData'
    
    const cachedData = localStorage.getItem(cacheKey)

    if (cachedData) {
        return JSON.parse(cachedData)

    // else fetch fresh data
    } else {
        const response = await fetch('https://swapi.dev/api/people/1/?format=json')
        const data = await response.json()
        localStorage.setItem(cacheKey, JSON.stringify(data))

        return data
    }
}

async function returnLukeSkywalker() {
    try {
        const forceData = await fetchJediMindMelt()
        // return Luke Skywalker from API response
        console.log(forceData.name)
        // return Luke Skywalker from local storage
        console.log(JSON.parse(localStorage.getItem('forceData')).name)
        
    } catch (error) {
        console.error(error)
    }
}

returnLukeSkywalker()

Pagination for API Responses

const indexurl: string = 'https://reqres.in/api/users?page='
const totalPages: number = 2

async function fetchUserData(url: string, page: number): Promise<string[]> {

    console.log(url+page)

    const response = await fetch(url+page)

    if(!response.ok) {
        throw new Error('ERROR :: Data could not be fetched from: ' + url)
    }

    const data = await response.json()

    console.log(data)

    const emails = data.data.map((user: any) => user.email)
    
    console.log(emails)

    return emails
}

async function fetchUserEmails(pages: number): Promise<string[][]> {
    const userEmailsByPage: string[][] = []

    for (let page = 1; page <= pages; page++) {
        const emails = await fetchUserData(indexurl, page)
        userEmailsByPage.push(emails)
        
        console.log(userEmailsByPage)
    }

    return userEmailsByPage
}

fetchUserEmails(totalPages)
    .then((userEmailsByPage) => {
        userEmailsByPage.forEach((emails, page) => {
            console.log('INFO :: Email addresses from page ', page+1, ': ', emails)
        })
    })
    .catch((error) => {
        console.error('ERROR :: ', error)
    })
INFO :: Email addresses from page  1 :  [
  'george.bluth@reqres.in',
  'janet.weaver@reqres.in',
  'emma.wong@reqres.in',
  'eve.holt@reqres.in',
  'charles.morris@reqres.in',
  'tracey.ramos@reqres.in'
]
INFO :: Email addresses from page  2 :  [
  'michael.lawson@reqres.in',
  'lindsay.ferguson@reqres.in',
  'tobias.funke@reqres.in',
  'byron.fields@reqres.in',
  'george.edwards@reqres.in',
  'rachel.howell@reqres.in'
]