TypeScript

How to Build an App with Node.js & TypeScript

Introduction

This tutorial will show you how to build an app with TypeScript.  We will walk you step-by-step thru setting up, developing and running a web application that queries the Internet for current crypto-currency price data. We’ll be using TypeScript in the React.js and Next.js frameworks.

If you are new to TypeScript you may want to first check out the introductory companion tutorial:

Note: If you want to use the public Internet to access Crypto data, when we get to the API Key section you will need to signup for a free account.

Legend

Symbols and formatting you will find in this document:

Command: This is something you type into the terminal.
Output: This is something your command will output in the terminal.

Note: This is a note that about the current topic being discussed.

Code text:  This is a key phrase that we have in our script files, or other system location.

Source file:

  This is content we have in one of the files of our application.

  Notice the buttons in the upper right corner of this block.

Quote: This is usually something that is quoted from referenced material.

Prerequisites

You will need to have the following things on your computer to take full advantage of this tutorial:

  • Node.js JavaScript runtime version 10.13 or later
  • Yarn package manager similar to npm
  • Terminal app (to run a shell / command line)
  • IDE or text editor (to edit code)
  • A basic understanding of programming
  • Some experience with JavaScript
  • How To Use An API With TypeScript

Note: the instructions given here for the command line are going to be from a Linux point of view but any forward-slash / based shell should work fine, for example: bash.

Install

Before we build an app with TypeScript we need to setup our environment.

Node.js

Node is the JavaScript runtime engine that powers the development server as well as the transpiling & bundling activities.

To see if you have it installed run this command in a shell:

node -v

It should come back with a version number:

v12.15.0

Otherwise you can download and install it from: nodejs.org.

Yarn

Yarn is a package manager that can control what software libraries your project installs and maintains.  If you don’t have it and can’t install it and know how to use npm, you can use that instead of Yarn.

To see if you have Yarn installed run this command in a shell:

yarn -v

It should come back with a version number:

1.22.4

Otherwise you can download and install it from: yarnpkg.com.

Note: Yarn 1 id preferred over Yarn 2 until the bugs in the newer version can be worked out.

Install starter app

We’re going to use Yarn to create a Next.js app called my-app with the default example code.  Type this command in the shell:

yarn create next-app my-app --example default

and you should get something back like this:

yarn create v1.22.4 [1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.13: The platform "linux" is incompatible with this module.
info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "create-next-app@9.4.4" with binaries:
- create-next-app
Creating a new Next.js app in RapidApi/my-app.
Installing react, react-dom, and next using yarn...
yarn add v1.22.4
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 474 new dependencies. ...
Done in 12.76s.
Success! Created my-app at RapidApi/my-app ...
We suggest that you begin by typing:
cd my-app
yarn dev
Done in 15.57s.

Note: Don’t worry if there are info messages and warnings about incompatible versions and deprecations. These are from third-party libraries that still work fine.

Dev Server

Change directories to our package dir and run the development server:

cd my-app && yarn dev

The output should look like:

yarn run v1.22.4
$ next dev
ready - started server on http://localhost:3000
event - compiled successfully
wait - compiling...
event - compiled successfully
event - build page: /next/dist/pages/_error
wait - compiling...
event - compiled successfully

View

Open a web browser and visit: http://localhost:3000 and you should see something that looks like:

Default App

Your directory structure should look like:

my-app/
├── node_modules/
├── package.json
├── pages/
│   ├── api/
│   │   └── hello.js
│   └── index.js
├── public/
│   ├── favicon.ico
│   └── vercel.svg
├── README.md
└── yarn.lock

Next.js creates automatic routing for us for all directories and files under the pages directory.  So the view we see at the root (http://localhost:3000/) is served by pages/index.js.

Let’s visit http://localhost:3000/api/hello to see the API route provided in the example code which we can do on the command line using the curl command followed by a URL:

curl "http://localhost:3000/api/hello"

Note: if you don’t have curl installed then you can also just use a web browser.

The response should be a JSON string:

{"name":"John Doe"}

TypeScript

Since we want to build an app with TypeScript let’s convert this package from JavaScript to TypeScript.

First stop the dev server with Ctrl-C (Linux) or Cmd-C (Mac).

Install Dependencies

Install the TypeScript dependency packages:

yarn add --dev typescript @types/react @types/node

Should give something like:

yarn add v1.22.4
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 5 new dependencies.
info Direct dependencies
├─ @types/node@14.0.26
├─ @types/react@16.9.43
└─ typescript@3.9.7
info All dependencies
├─ @types/node@14.0.26
├─ @types/prop-types@15.7.3
├─ @types/react@16.9.43
├─ csstype@2.6.11
└─ typescript@3.9.7
Done in 23.21s.

Note: again don’t worry too much about incompatibilities.

Config

Create an empty TypeScript configuration file:

touch tsconfig.json

Now when we start the dev server:

yarn dev

It will populate the tsconfig.json file and create the next-env.d.ts file, which ensures Next.js types are picked up by the TypeScript compiler:

yarn run v1.22.4
$ next dev
ready - started server on http://localhost:3000
We detected TypeScript in your project and created a tsconfig.json file for you.
Your tsconfig.json has been populated with default values.
event - compiled successfully

Our project is now ready to write TypeScript.

Conversion

So let’s convert our files from JavaScript to TypeScript.

mv pages/index.js pages/index.tsx
mv pages/api/hello.js pages/api/hello.ts

Note: when we make changes to almost any of the files in our package, for example when we save a TypeScript file, Next.js will detect these changes and hot-reload the browser page for us automatically. This is the case provided we have the dev server running and a web browser open with a localhost:3000 page loaded.

Make sure the index page and the hello page are still loading properly.

Code

Finally, some code!

Ok, so we got our environment setup, our Next.js app is running and we’re serving up a web page and an API endpoint. But this is what we get out of the box from the example app.

Let’s write our crypto app.

Home Page

First we’ll create a form to use for submitting our query parameters:

pages/index.tsx:

import Head from 'next/head'

export default function Home() {
  return (
    <div className="container">
      <Head>
        <title>Crypto Prices</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <h3 className="title"> Crypto Prices </h3>

        <div className="grid">
          <div className="form card">
            <form lang="en">
              <div>
                <div>
                  <label htmlFor="symbol">Symbol:</label>
                  <input type="text" id="symbol" name="symbol" data-default="BTC" placeholder="BTC" />
                </div>
                <div>
                  <label htmlFor="currency">Currency:</label>
                  <input type="text" id="currency" name="currency" data-default="USD" placeholder="USD" />
                </div>
                <div>
                  <label htmlFor="exchange">Exchange:</label>
                  <input type="text" id="exchange" name="exchange" data-default="Kraken" placeholder="Kraken" />
                </div>
              </div>
              <div className="submit">
                <button className="submit">Get Prices</button> &rarr;
              </div>
            </form>
          </div>

          <div className="code card">
            <ul>
              <li>Prices</li>
            </ul>
          </div>
        </div>
      </main>
    </div>
  )
}

Start the dev server back up and visit localhost:3000:

Unstyled Form

Style

This could use a bit of styling.  The easiest way is to add a global wrapper module.  Add a new file called _app.tsx in the pages directory:

pages/_app.tsx:

import { AppProps } from 'next/app'
import '../styles/global.css'

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

Notice that we are declaring that our App function parameter is of type AppProps.  We just wrote our first TypeScript type. Also notice that we are importing a global stylesheet.  Let’s write that now.  Create a new top-level directory called styles and then create a file inside of it:

styles/global.css:

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
    Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
    sans-serif;
}

* {
  box-sizing: border-box;
}

.container {
  min-height: 100vh;
  padding: 0 0.5rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

main {
  padding: 5rem 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.title a {
  color: #0070f3;
  text-decoration: none;
}

.title a:hover,
.title a:focus,
.title a:active {
  text-decoration: underline;
}

.title {
  margin: 0;
  line-height: 1.15;
  font-size: 2rem;
  text-align: center;
}

code {
  font-family: Menlo, Monaco, Lucida Console, Liberation Mono,
    DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
}

.grid {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 3rem;
}

.card {
  margin: 1rem;
  padding: 1.5rem;
  text-align: left;
  color: inherit;
  text-decoration: none;
  border: 1px solid #eaeaea;
  border-radius: 10px;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.card:hover,
.card:focus,
.card:active {
  color: #0070f3;
  border-color: #0070f3;
}

@media (max-width: 600px) {
  .grid {
    width: 100%;
    flex-direction: column;
  }
}

input {
  padding-left: 9px;
  margin-bottom: 9px;
}

div.submit {
  margin-top: 9px;
  text-align: right;
}
button.submit {
  background-color: #0070f3;
  border-radius: 6px;
  border: 1px solid #337fed;
  display: inline-block;
  cursor: pointer;
  color: #ffffff;
  font-weight: bold;
  padding: 6px 24px;
}
button.submit:hover {
  background-color:#1e62d0;
}
button.submit:active {
  position:relative;
  top:1px;
}

.card.form {
  max-width: 300px;
}

.card.code ul li {
  overflow-wrap: anywhere;
  padding-bottom: 10px;
}

Your directory structure should now look like:

my-app/
├── next-env.d.ts
├── node_modules/
├── package.json
├── pages/
│   ├── api/
│   │   └── hello.ts
│   ├── _app.tsx
│   └── index.tsx
├── public/
│   ├── favicon.ico
│   └── vercel.svg
├── README.md
├── styles/
│   └── global.css
├── tsconfig.json
└── yarn.lock

Note: Whenever we add an _app.tsx file Next.js needs for the dev server to be restarted for the changes to take effect.

If you have the dev server running, stop it with Ctrl-C (Linux) or Cmd-C (Mac).  Then restart it again with yarn dev.

Now our web page should be stylin’:

Styled Form

Hello API

Next let’s get that button in our client (browser) to call our API endpoint on our server.

Wait a minute, why not just call the external endpoint from the browser?

Why do we need the server to call the external endpoint?  Can’t the browser just call it?

Ah, these are good questions.  And the answer is that the browser has built-in security that tries to prevent what are called Cross-Origin Resource requests.  In short, script from our domain cannot make JavaScript calls to another domain.

For security reasons, browsers restrict cross-origin HTTP requests initiated from scripts. For example, XMLHttpRequest and the Fetch API follow the same-origin policy. This means that a web application using those APIs can only request resources from the same origin the application was loaded from unless the response from other origins includes the right CORS headers. —MDN

Note: If we had to, we could use CORS in the browser but making the request from the server doesn’t have those same security issues.

Index Module

How do we get that button to call our endpoint?

We’ll do this by making some changes to our index module.

  1. Firstly, add a new import to the top of the file:

pages/index.tsx:

import { useState } from 'react'
  1. Secondly, add a React State Hook for a new variable we’ll call prices.
export default function Home() {
  const [prices, setPrices] = useState([]);
  ...
  1. Then add a function called getPrices to the top of our Home component.
export default function Home() {
  ...
  async function getPrices(e): Promise<void> {
    e.preventDefault() // prevent page from submitting form
    const result = await (await fetch('/api/hello')).text()
    setPrices(prices.concat(result))
  }
  ...

getPrices will be an “asynchronous” function because it uses the async keyword. Asynchronous functions always return a Promise type.  The Promise will eventually resolve to a void type since the function does not have a return statement.

TypeScript’ing

This is some wonderful TypeScript’ing here.  The stuff between the colon : and open-brace { is the type annotation, in this case Promise<void>, basically when you see a colon in a TypeScript program, get ready for the type annotation:

getPrices(e): Promise<void> {

More recent additions to the JavaScript language are async functions and the await keyword… These features basically act as syntactic sugar on top of promises, making asynchronous code easier to write and to read afterwards. They make async code look more like old-school synchronous code, so they’re well worth learning. This article gives you what you need to know. —MDN

Note: If you want to learn async programming the Mozilla Developer Network has laid out a bunch of guides to take you thru it step-by-step.  It is titled, Asynchronous JavaScript.

What the heck is this double await await thing?

const result = await (await fetch('/api/hello')).text()

The await operator is used to wait for a Promise. First we await the fetch for the /api/hello endpoint then when that resolves we await the call to text().

If you’re new to async programming and this seems a bit confusing, don’t worry, it is.  The traditional callback way to do the same thing is not as elegant, and is affectionately referred to as “callback hell“.

Moving On

We’ll need to get the button to call our price function.

  1. Therefore, let’s add the onClick handler:
<div className="submit">
  <button className="submit" onClick={getPrices}>Get Prices</button> &rarr;
</div>
  1. And we’re also going to want to bind our display list to the prices variable:
<div className="card code">
  <ul>
    {prices.map((price, i) => <li key={i}>{price}</li>)}
  </ul>
</div>

Now the index module should look like this:

pages/index.tsx:

import { useState } from 'react'
import Head from 'next/head'

export default function Home() {

  const [prices, setPrices] = useState([]);

  async function getPrices(e): Promise<void> {
    e.preventDefault() // prevent page from submitting form
    const result = await (await fetch('/api/hello')).text()
    setPrices(prices.concat(result))
  }

  return (
    <div className="container">
      <Head>
        <title>Crypto Prices</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <h3 className="title"> Crypto Prices </h3>

        <div className="grid">
          <div className="form card">
            <form lang="en">
              <div>
                <div>
                  <label htmlFor="symbol">Symbol:</label>
                  <input type="text" id="symbol" name="symbol" data-default="BTC" placeholder="BTC" />
                </div>
                <div>
                  <label htmlFor="currency">Currency:</label>
                  <input type="text" id="currency" name="currency" data-default="USD" placeholder="USD" />
                </div>
                <div>
                  <label htmlFor="exchange">Exchange:</label>
                  <input type="text" id="exchange" name="exchange" data-default="Kraken" placeholder="Kraken" />
                </div>
              </div>
              <div className="submit">
                <button className="submit" onClick={getPrices}>Get Prices</button> &rarr;
              </div>
            </form>
          </div>

          <div className="code card">
            <ul>
              {prices.map((price, i) => <li key={i}><code>{price}</code></li>)}
            </ul>
          </div>
        </div>
      </main>
    </div>
  )
}

So now when we click the Get Prices button it should call our API and display John Doe:

Form Calls Hello API

The Heart

Finally, we’re getting to the heart of the matter: creating and calling a new API endpoint that goes out on the Internet to get the current crypto prices.

API Key

We will use the Crypto Asset Market Data API from BlockFacts and like most API services on the Internet we are required to signup in order to obtain an API Key.  We supply this key to their endpoints with every request.  It is used to track us to make sure we are abiding by their Terms Of Service.  Basically they want to make sure we’re not spamming them too quickly.

Note: If you need to brush-up on using APIs read the section Testing the API from the previous tutorial.

RapidAPI

Go to RapidAPI and signup for a free account and head over to the dashboard.

Currency Trade Dashboard

You should now be looking at the Crypto Asset Market Data API Current trade data endpoint dashboard. In the Code Snippets section: go to the code selector drop-down and select (Shell) cURL then click Copy Code.

Paste this code into your terminal (you should paste yours, the example below does not have a valid API key):

curl --request GET 
--url 'https://crypto-asset-market-data-unified-apis-for-professionals.p.rapidapi.com/api/v1/exchanges/trades?exchange=Kraken&asset=BTC&denominator=USD' 
--header 'x-rapidapi-host: crypto-asset-market-data-unified-apis-for-professionals.p.rapidapi.com' 
--header 'x-rapidapi-key: [X-RAPIDAPI-KEY GOES HERE]'

and you should get back some pricing data:

{"BTC-USD":[{"exchange":"KRAKEN","pair":"BTC-USD","price":9730.7,...}]}

Note: Keep your x-rapidapi-key somewhere as we will need it for later.

Prices Endpoint

We’re going to create a new endpoint so let’s first remove the old hello endpoint:

rm pages/api/hello.ts

And add a new endpoint:

pages/api/prices.ts:

export default async function (req, res) {
  res.status(200).send('Helpful information')
}

Then we need to add a new function to our index module to call this new endpoint:

pages/index.tsx:

async function fetcher() {
  const res = await fetch('/api/prices', {
    method: 'GET',
  })
  return await res.text()
}

Also update the getPrices function to call our new fetcher:

async function getPrices(e) {
  e.preventDefault() // prevent page from submitting form
  const result = await fetcher()
  setPrices(prices.concat(result))
}

Make sure your dev server is running, click the Get Prices button and the output should show our helpful information:

Helpful Information

Post Data

So that was a simple HTTP GET request.  Now let’s instead do an HTTP POST request and send our form data in the request body.

Update our fetcher function to POST the data to the prices endpoint instead of GET.  Notice how it now accepts a data object to be posted in the body of the request:

pages/index.tsx:

async function fetcher(data: object) {
  const res = await fetch('/api/prices', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify(data) // body data type must match "Content-Type" header
  })
  return await res.text()
}

Update getPrices to include the form data in the fetcher call:

async function getPrices(e): Promise<void> {
  e.preventDefault()
  const result = await fetcher({
    currency: getValue('currency'),
    exchange: getValue('exchange'),
    symbol: getValue('symbol'),
  })
  setPrices(prices.concat(result))
}

And also include that getValue helper function which grabs the data from the HTML form inputs:

function getValue(name: string): string {
  const elements = document.getElementsByTagName('form')[0].elements
  const element = elements.namedItem(name) as HTMLInputElement
  return element.value || element.dataset.default
}

Now that our index module is posting the form data we need to update the prices module to allow both GET and POST:

pages/api/prices.ts:

async function get(req, res): Promise<any> {
  return 'Helpful information'
}

async function post(req, res): Promise<any> {
  return req.body // Simply returning our request body
}

export default async function (req, res): Promise<any> {
  switch (req.method) {
    case 'GET':
      res.status(200).send(await get(req, res))
      break
    case 'POST':
      res.status(200).json(await post(req, res))
      break
    default:
      res.status(405).end() //Method Not Allowed
  }
}

Click the Get Prices button and the endpoint should now give us back the data that we are sending:

Posted Data

Note: Try it out. Enter some data in the boxes and see the server send it back to you.

External Data

Here it is the final push for real data.

In our prices module update the post function to actually send our query data to the external RapidAPI endpoint:

pages/api/prices.ts:

async function post(req, res): Promise<any> {
  const params = {
    denominator: req.body.currency,
    exchange: req.body.exchange,
    asset: req.body.symbol,
  };
  const priceUrl = new URL('https://crypto-asset-market-data-unified-apis-for-professionals.p.rapidapi.com/api/v1/exchanges/trades');
  priceUrl.search = (new URLSearchParams(params)).toString();

  const remoteResponse = await external(
    priceUrl.toString(),
    {
      headers: {
        'x-rapidapi-host': 'crypto-asset-market-data-unified-apis-for-professionals.p.rapidapi.com',
        'x-rapidapi-key': '[YOUR X-RAPIDAPI-KEY]',
      }
    }
  )
  const response = Object.assign({
    currency: req.body.currency,
    exchange: req.body.exchange,
    symbol: req.body.symbol,
  }, remoteResponse)

  return response
}

Replace the string [YOUR X-RAPIDAPI-KEY] with your actual x-rapidapi-key that we saved from earlier.

Also add the simple external helper function:

async function external(url: string, config?: object): Promise<any> {
  const res = await fetch(url, config)
  const data = await res.json()
  return data
}

That should be it.

Click the Get Prices button now and you should be rewarded with current prices:

External Price Data

Types

The final thing we’ll cover before wrapping up is annotating some of our variables and functions with some home-made types.  Since our application makes liberal use of the global namespace we’ll create a global TypeScript Declaration File.

Create a top-level file:

global.d.ts:

declare namespace my {

  interface ICryptocompareResult {
    [string]: any
  }

  type CryptocompareResult = ICryptocompareResult | void

  interface IPriceResult {
    currency: string
    exchange: string
    symbol: string
    [string]: any
  }

  type PriceResult = IPriceResult | void

}

This file is imported automatically by TypeScript so all our modules can use these declarations without any explicit importing, easy.

…if you have beginner TypeScript developers you can give them a global.d.ts file to put interfaces / types in the global namespace to make it easy to have some types just magically available for consumption in all your TypeScript code.  —TypeScript Deep Dive

Declaring the my namespace is not strictly necessary but in a real-world application we should try to avoid polluting the global namespace if we can.

Let’s first annotate our index module:

  • give the fetcher function a return type
  • change the actual data returned from text to json
  • this also means that in getPrices we need to expect an object
  • annotate the result constant type also in getPrices
  • give getPrices a return type

pages/index.tsx:

async function fetcher(data: object): Promise<my.PriceResult> {
  ...
  return await res.json()
}
async function getPrices(e): Promise<void> {
  ...
  const result: my.PriceResult = await fetcher({...})
  setPrices(prices.concat(JSON.stringify(result)))
}

And last but not least annotate the prices module as shown in the lines below:

  • import the Next.js API types
  • each function gets annotated
  • also remoteResponse variable

pages/api/prices.ts:

import { NextApiRequest, NextApiResponse } from 'next'

async function external(url: string, config?: object): Promise<my.CryptocompareResult> {
  ...
}

async function get(req: NextApiRequest, res: NextApiResponse<string>): Promise<string> {
  ...
}

async function post(req: NextApiRequest, res: NextApiResponse<my.PriceResult>): Promise<my.PriceResult> {
  ...
  const remoteResponse: my.CryptocompareResult = await external(...)
  ...
}

export default async function (req: NextApiRequest, res: NextApiResponse<string | my.PriceResult>): Promise<any> {
  ...
}

If we’ve done everything correctly then our system should still be working.

Wrapping Up

I hope you enjoyed this tutorial and perhaps learned something about how to build an application with TypeScript.  Next.js seems like a good framework to write quick APIs as you get routing built-in with your directory structure, and that’s nice. So try running the query with other coins and exchanges.

If you want to experiment with other APIs be sure to check out the RapidAPI Marketplace. And of course, if you have any questions be sure to leave them in the comments below.

GitHub Project

If you would like to see the final version of the project have a look at this GitHub repository.

5/5 - (2 votes)
Share
Published by

Recent Posts

Power Up Your Enterprise Hub: New March Release Boosts Admin Capabilities and Streamlines Integrations

We're thrilled to announce the latest update to the Rapid Enterprise API Hub (version 2024.3)!…

2 weeks ago

Unveiling User Intent: How Search Term Insights Can Empower Your Enterprise API Hub

Are you curious about what your API consumers are searching for? Is your Hub effectively…

2 weeks ago

Rapid Enterprise API Hub Levels Up Custom Branding, Monetization, and Management in February Release

The RapidAPI team is excited to announce the February 2024 update (version 2024.2) for the…

4 weeks ago

Supercharge Your Enterprise Hub with January’s Release: Search Insights, Login Flexibility, and More!

This January's release brings exciting features and improvements designed to empower you and your developers.…

3 months ago

Enhanced Functionality and Improved User Experience with the Rapid API Enterprise Hub November 2023 Release

Rapid API is committed to providing its users with the best possible experience, and the…

5 months ago

The Power of Supporting Multiple API Gateways in an API Marketplace Platform

In today's fast-paced digital world, APIs (Application Programming Interfaces) have become the backbone of modern…

6 months ago