Cover Page
Backend Page
TypeScript with Express
Install Node.js
We will first use the apt package manager to install Node.js version 18.19.1 or later (up to version 24.5.0 tested) and its tools:
server$ sudo apt update
server$ sudo apt install npm # will automatically install node.js also
server$ sudo npm install -g pnpm@latest-10
Confirm that you’ve installed Node.js version 18.19.1 or later:
server$ node --version
# output:
v18.19.1
# or later
Installing other versions of Node.js
To install Node.js v24:
server$ curl -sL https://deb.nodesource.com/setup_24.x | sudo bash -
server$ sudo apt update
server$ sudo apt install nodejs # automatically install npm also
For other versions of Node.js, replace 24 with the version number you want.
To remove Node.js installed from nodesource:
server$ sudo apt purge nodejs
server$ sudo rm /etc/apt/sources.list.d/nodesource.list
See documentations on How to Install Node.js on Ubuntu and How to remove nodejs from nodesource.com
package.json and tsconfig.json
First create and change into a directory where you want to keep your chatterd package files:
server$ mkdir ~/reactive/chatterd
server$ cd ~/reactive/chatterd
Create the file package.json with the following content:
{
"type": "module"
}
This allows use of import statements instead of require() of CommonJS.
Install the following packages, including the pm2 process manager:
server$ pnpm install express http-status-codes http2-express morgan
server$ pnpm install -D @types/express @types/morgan @types/node
server$ pnpm install -g pm2
Your ~/reactive/chatterd directory should now contain the following:
node_modules/ package.json pnpm-lock.yaml
The package.json file should contain:
{
"type": "module",
"dependencies": {
"express": "^4.21.2",
"http2-express": "^1.1.0",
"morgan": "^1.10.1",
},
"devDependencies": {
"@types/express": "^5.0.2",
"@types/morgan": "^1.9.9",
"@types/node": "^24.3.0",
}
}
The devDependencies are used in development, not deployed in production. In our case, all the devDependencies are for static type checking. The dependencies with @types prefix define the types
used in their corresponding native JavaScript modules that have no static typing information. The file package-lock.json locks the dependencies to specific versions, to prevent automatically incorporating later, breaking versions. The node_modules directory contains the module files of both direct dependencies in package.json and indirect dependencies used by the direct dependencies.
Package update
Updating packages downloaded from the registry:
server$ pnpm outdated
server$ sudo pnpm update # -g # to update globally
See documentations on How to Install Node.js on Ubuntu and How to remove nodejs from nodesource.com
Create a tsconfig.json configuration file for use by TypeScript:
server$ vi tsconfig.json
and put the following lines in it:
{
"compilerOptions": {
// https://aka.ms/tsconfig for definitions
/* Language and Environment */
"target": "ES2022", // version of emitted JavaScript and compatible library
"module": "ES2022", // version of generated module code
"moduleResolution": "bundler", // how TypeScript looks up a file from module
/* Interop Constraints */
"verbatimModuleSyntax": true, // transform and elide only imports/exports marked
// "type"; the rest are emitted per the "module" setting
"allowSyntheticDefaultImports": true, // can 'import x from y' even if y has no default export
"esModuleInterop": true, // can import CommonJS modules;
// enables 'allowSyntheticDefaultImports'
/* Type Checking */
"alwaysStrict": true, // always enable all strict type-checking options
/* Completeness */
"skipLibCheck": true // skip type checking all .d.ts files
}
}
Save and exit tsconfig.json.
You now have all of the packages needed by the Chatter backend.
chatterd app
Create a file called main.ts:
server$ vi main.ts
We will put the server and URL routing code in main.ts. Open and edit the file to add the following import lines:
import express from 'express' // import default, not incl. types
import http2Express from 'http2-express'
import morgan from 'morgan'
import { readFileSync } from 'node:fs' // import function
import { createSecureServer } from 'node:http2'
import type { AddressInfo } from 'node:net' // import type
import * as handlers from './handlers.js' // import namespace
process.on('SIGTERM', () => {
process.exit(0)
})
process.on('uncaughtException', (err) => {
console.error(`Uncaught exception: ${err}\n` + `Stack trace: ${err.stack}`);
process.exit(2);
});
We also took the opportunity to register two callback functions with the process: (1) to shutdown gracefully when the process is terminated by user, and (2) to print out a stack trace and exit with an error code in the case of any uncaught exception.
With process termination events taken care of, we next create an express
instance and define routes to serve Chatter’s
try {
// the rest of main.ts goes here
} catch (error) {
console.log(error)
process.exit(1)
}
Inside the try block, we next create an express
instance and define a route to serve llmprompt’s single API: HTTP POST request with URL endpoint llmprompt. We route this endpoint to the llmprompt() function. With each route, we implicit specify which HTTP method is allowed for that URL endpoint by providing only route definition with get() or post() method according to whether the endpoint accepts an HTTP GET or POST request. Replace // the rest of main.ts goes here with:
const app = http2Express(express)
.use(morgan('common')) // logging
.use(express.json())
.post('/llmprompt/', handlers.llmprompt)
// setup tls and create server
We have also configured our express app with two middlewares: morgan to log each response and
express.json() to automatically parse incoming requests containing JSON payload and making it available as the Request’s body to the URL handler. We also wrapped express inside the http2Express() bridge to allow interoperability with HTTP/2, though it seems to work only with Express up to v. 4.21.2, not v. 5.1.0. This is a workaround until HTTP/2 is eventually supported natively in express. Express v. 5.1.0 doesn’t seem to work with HTTP/2.
The function llmprompt() will be implemented in handlers.ts later.
Staying inside the try block, after setting up the express app, we set up the node.js server:
- create an instace of the secure server with the
chatterdcertificate and key we created earlier, - load up the
appinstance ofexpressto the secure server, - bind it to the wildcard IP address (
0.0.0.0, equivalent ofany) and the default HTTPS port (443), and launch it. Replace// setup tls and create serverwith:const tls = { key: readFileSync("/home/ubuntu/reactive/chatterd.key"), cert: readFileSync("/home/ubuntu/reactive/chatterd.crt"), allowHTTP1: true } const server = createSecureServer(tls, app).listen({host: '0.0.0.0', port: 443}, () => { const address = server.address() as AddressInfo console.log(`chatterd on https://${address.address}:${address.port}`) })
We’re done with main.ts. Save and exit the file.
handlers.ts
We implement the URL path API handler module in handlers.ts:
server$ vi handlers.ts
Start the file with the following imports:
import type { Request, Response } from 'express'
import HttpStatus from 'http-status-codes'
import { pipeline } from 'stream/promises'
We next set the Ollama base URL string and specify the handler llmprompt(), which
simply forwards user prompt from the client to Ollama’s generate API and passes
through Ollama’s reply NDJSON stream to the client using the pipeline function
from the stream package:
const OLLAMA_BASE_URL = "http://localhost:11434/api"
export async function llmprompt(req: Request, res: Response) {
try {
const response = await fetch(OLLAMA_BASE_URL+"/generate", {
method: req.method,
body: JSON.stringify(req.body),
})
res.setHeader('Content-Type', response.headers.get('content-type'))
res.status(response.status)
await pipeline(response.body, res)
} catch (error) {
res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(error)
return
}
}
We’re done with handlers.ts. Save and exit the file.
Build and Test run
![]()
TypeScript is a compiled language, like C/C++ and unlike JavaScript and Python, which are an interpreted languages. This means you must run npx tsc each and every time you made changes to your code, for the changes to show up when you run node.
To build your server, transpile TypeScript into JavaScript:
server$ npx tsc
You should now see a .js file for each .ts file in the directory.
To run your server:
server$ sudo node main.js # NOTE `.js` not `.ts`
# Hit ^C to end the test
You can test your implementation following the instructions in the Testing Chatter APIs section.
References
- 7 Simple Steps to build your own TypeScript Node.js project
- A Complete Guide to HTTP/2 in Node.js
- morgan
- Mastering Error Handling in JavaScript
- Production best practices: performance and reliability
| Prepared by Chenglin Li, Xin Jie ‘Joyce’ Liu, Sugih Jamin | Last updated October 30th, 2025 |