TypeScript is a popular tool used by web developers create scalable web applications. In this blog post, we will look at how TypeScript, combined with Now 2, can enhance and empower developer workflows.

Benefits of TypeScript

TypeScript, a superset of JavaScript, adds a layer of static typing to JavaScript. The TypeScript compiler checks an application for type soundness and proceeds to emit JavaScript. Some of the benefits of TypeScript are:
  • Cumulative Adoption: any JavaScript project can be converted to a TypeScript project in a cumulative manner.
  • Autocompletion: Most IDEs use type inference to provide a number of developer conveniences such as autocomplete and auto-import, one example being Visual Studio Code's IntelliSense.
  • Type Soundness: TypeScript defines clear contracts between components in an application, making impossible states impossible and ensuring API compatibility.
  • Superset of JavaScript: All valid JavaScript is valid TypeScript. TypeScript's syntax is nearly identical to JavaScript, with the exception of a few keywords: interface, type, as, and type annotation syntax.

The Project

To demonstrate some of the powerful aspects of Now 2, we will work in a majestic monorepo to put together a backend and a frontend. Both parts of the application will use TypeScript and share a common type between backend and frontend. See it in action here.



An idea for the application

While the application demonstrates only a limited subset of the stack mentioned in the original tweet, we have deliberately made this decision in order to focus and highlight TypeScript only for this article. We will produce a more involved example with a broader tech stack in a future article.

Under the Hood

Before we get into the code, let us first create a way for us to test our lambdas locally.

Local Development

As with any application, it would be incredibly convenient to develop a lambda locally. Lambdas are request handlers that have an API identical to the first argument of http.createServer: they are functions that get the request and response objects as arguments.
Since they are functions that are exported, executing them locally with node does not do anything: it exports a function and the process exits. On Now, the exported function is executed in the context of a complete cloud infrastructure.
We can mimic this behavior locally by wrapping our handler in http.createServer only if they are not executing inside of Now. We can do this with Now environment variables. Our team is currently working on now dev as an enhancement to this workflow.
For now, let us set an environment variable only on Now. We will come back to this later when we deploy our app.
{
  "version": 2,
  "env": {
    "IS_NOW": "true"
  }
}

Setting an environment variable that will be present only on Now using a now.json file at the root

In our app, we can check if it is currently executing on Now, or locally as follows. If it is running on Now, we do not require a server (it's serverless). Instead, we simply export the function. If our code is running locally, we spin up a server and listen on port 3000.
import { createServer, IncomingMessage, ServerResponse } from 'http'

const handler = (_: IncomingMessage, res: ServerResponse) => {
  res.end('Hello World!')
}

// process.env.IS_NOW is undefined locally,
if (!process.env.IS_NOW) {
  // so we have a server with the handler!
  createServer(handler).listen(3000)
}

// Either way, this is exported
// On Now, this is what gets invoked/called/executed.
export default handler

An example file that demonstrates creating a server with our handler for local debugging. This step is skipped on Now

We are now able to run ts-node on the file described above, and our handler gets invoked when we visit http://localhost:3000. Great, we can now develop locally. Let us start with a backend.

Backend

We want a simple backend that is able to deliver JSON objects to our frontend. However, since we are using TypeScript, we would like to take full advantage of the type-safety available to us. Let us define an interface for the type of data we will be sending to our frontend.
// types.d.ts
export interface Sushi {
  type: 'maki' | 'temaki' | 'uramaki' | 'nigiri' | 'sashimi'
  title: string
  description: string
  pictureURL: string
}

types.d.ts; an interface that describes an entity that we will work with across our application

The backend and frontend will share this type to make sure they work with consistent data. Let us implement a backend handler.
// src/backend/get-sushi/index.ts
import { IncomingMessage, ServerResponse } from 'http'
import url from 'url'

// The interface we just defined.
import { Sushi } from '../../../types'

// Could be a function that calls a DB.
import { getSushi } from './getSushi'

const handler = (req: IncomingMessage, res: ServerResponse) => {
  res.writeHead(200, { 'Content-Type': 'application/json' })

  /**
   * Parse the query string.
   * The API will be /api/sushi?type=SOME_TYPE
   */
  const { type } = url.parse(req.url || '', true).query
  res.end(JSON.stringify(getSushi(type)))
}

if (!process.env.IS_NOW) {
  createServer(handler).listen(3000)
}

export default handler

The backend API layer of our app within src/backend/get-sushi/index.ts

This is our backend API. It parses a query string and looks for a type parameter, signaling the type of sushi a user is querying for. From there, it passes this value to the getSushi function, whose stringified output is returned to the user.

Incompatible types cause real-time compiler errors

With this code in a given editor, we'll notice that the type variable is red. Not good. What's happening here is that the type of type is inferred from url.parse().query, which is a ParsedUrlQuery. getSushi expects an argument of type Sushi["type"], which is a union type of various types of sushi.
How can we remedy this? Let us write a validator.
// Take in a ParsedUrlQuery, and output a type of sushi.
const validateQuery = (query: ParsedUrlQuery): Pick<Sushi, 'type'> => {
  if (!query.type || Array.isArray(query.type)) {
    throw Error('Invalid query string')
  }

  if (availableTypesOfSushi.indexOf(query.type) === -1) {
    throw Error('Sushi not found 🤔')
  }

  return query as Pick<Sushi, 'type'>
}

An excerpt from our backend implementation that demonstrates a way to ensure a type sound query parameter.

Now, the TypeScript compiler knows that this function always outputs an object of the correct shape. When we use this function inside of getSushi, the file should no longer have any compilation errors and we now have our type-safe backend in place.
- res.end(JSON.stringify(getSushi(type)))
+ res.end(JSON.stringify(getSushi(validateQuery(type))))

Validating the query parameters before passing them to our getter

Frontend

For the frontend, we'll use node-fetch to fetch data from our backend and display it.
// src/frontend/index.ts
import { createServer, IncomingMessage, ServerResponse } from 'http'
import fetch from 'node-fetch'
import url from 'url'

import { Sushi } from '../../types' // <- our shared type
import { sushiLayout, errorLayout } from '../layout'

const handler = async (req: IncomingMessage, res: ServerResponse) => {
  /**
   * Get the query string from
   * typescript-sushi.now.sh/?type=sashimi
   */
  const { type } = url.parse(req.url || '', true).query

  res.writeHead(200, { 'Content-Type': 'text/html' })

  try {
    const sushiResponse = await fetch(
      'https://typescript-sushi.now.sh/api/get-sushi?type=' + type
    )

    const { description, pictureURL, title }: Sushi = await sushiResponse.json()
    res.end(sushiLayout(type))
  } catch (e) {
    res.end(errorLayout(e))
  }
}

if (!process.env.IS_NOW) {
  createServer(handler).listen(3000)
}

export default handler

src/frontend/index.ts; The frontend implementation of our application

In this way, we are able to share an interface across the frontend and backend of our TypeScript application and ensure the structure of the objects that we work with. At scale, this has been proven to provide significant wins for teams, preventing runtime errors and increasing confidence and developer velocity.

Deploying

Now 2 builds the TypeScript in the cloud using builders powered by ncc. It also handles the concern of routing, mapping our backend and frontend lambdas to accessible URLs. Let us set up these rules in now.json at the root of our project.
{
  "version": 2,
  "env": {
    "IS_NOW": "true"
  },
  "builds": [
    {
      "src": "./src/**/*.ts",
      "use": "@now/node@canary"
    }
  ],
  "routes": [
    { "src": "/api/(.*)", "dest": "/src/backend/\$1" },
    { "src": "/sushi/(.*)", "dest": "/src/frontend/sushi?type=\$1" },
    { "src": "/(.*)", "dest": "/src/frontend/\$1" }
  ]
}

The final now.json for our project

In addition to our environment variable, we instruct now to use the @now/node@canary builder for any .ts files recursively inside ./src, and to map visits to /api/SOMETHING to /src/backend/SOMETHING, where our backend lambdas live. We do similar configuration for other routes of our application.
With this configuration, we are now ready to put this project online.
$ now

Deploying our project with Now

Our project is deployed and ready to be used in an effort to educate the world about various kinds of sushi. 🎉 🍣

Conclusion

Making development accessible is at the heart of all that we do at ZEIT. With ncc and our upcoming @now/node builder, we are able to do exactly this as we enable our developer community to create and deploy scalable applications with TypeScript on Now.
A live preview of the app is deployed on Now. We look forward to seeing more exciting applications emerge using this technology. Write us on Twitter to let us know about your next project.
Happy type-safe coding!