Typescript for Web APIs
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'
]