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'
]