Benefits of TypeScript
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
An idea for the application
Under the Hood
Local Development
http.createServer
: they are functions that get the request
and response
objects as arguments.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.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.{ "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
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
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
// 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
// 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
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
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.// 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.
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
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
Deploying
{ "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
@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.$ now
Deploying our project with Now
Conclusion
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.