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.
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

The source code of this project is available in our now-examples repository. 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"
  }
}
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
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
}
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
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.
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'>
}
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))))

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
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" }
  ]
}
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
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.
The source code for this example is publicly available on GitHub. A live preview is also 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!