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 version10.13
or laterYarn
package manager similar tonpm
- 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:
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> → </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:
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’:
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.
- Firstly, add a new import to the top of the file:
pages/index.tsx
:
import { useState } from 'react'
- Secondly, add a React State Hook for a new variable we’ll call
prices
.
export default function Home() { const [prices, setPrices] = useState([]); ...
- 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.
- Therefore, let’s add the
onClick
handler:
<div className="submit"> <button className="submit" onClick={getPrices}>Get Prices</button> → </div>
- 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> → </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:
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.
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:
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:
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:
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
tojson
- this also means that in
getPrices
we need to expect an object - annotate the
result
constant type also ingetPrices
- 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.
Leave a Reply