Skip to main content
Version: Next

Platformatic Node

The Platformatic Node allows to run a Fastify, Express, Koa or plain Node application as a Platformatic Runtime application with no modifications.

Getting Started

Create or copy your application inside the applications, services or web folder. If you are not using autoload, you also have to explictly add the new application.

You are all set, you can now start your runtime as usual via wattpm dev or wattpm start.

Install

npm install @platformatic/node

Example configuration file

Create a watt.json in the root folder of your application with the following contents:

{
"$schema": "https://schemas.platformatic.dev/@platformatic/node/2.0.0.json",
"application": {
"basePath": "/frontend"
}
}

Specify application info

Some info can be specified for Node.js applications through the @platformatic/globals Runtime APIs. Currently, a few lines of code must be added.

OpenAPI and GraphQL schema

It's possible for the node applications to expose the OpenAPI or GraphQL schemas, if any. This can be done adding few lines of code, e.g. for fastify:

import { getLogger, setOpenapiSchema } from '@platformatic/globals'
import fastify from 'fastify'
import fastifySwagger from '@fastify/swagger'

export async function build () {

const server = fastify({
loggerInstance: getLogger()
})

await server.register(fastifySwagger, {
openapi: {
openapi: '3.0.0',
info: {
title: 'Test Fastify API',
description: 'Testing the Fastify swagger API',
version: '0.1.0'
},
}
})

server.addHook('onReady', async () => {
const schema = server.swagger()
setOpenapiSchema(schema)
})

Connection String

It's possible to specify if a node application uses a connection string (and which one). This is useful to map which application uses which database and to potentialy track database changes.

import { setConnectionString } from '@platformatic/globals'
import { createServer } from 'node:http'

setConnectionString('postgres://dbuser:dbpass@mydbhost/apidb')

const server = createServer((_req, res) => {
res.end(JSON.stringify({ ok: true }))
})

server.listen(0)

Architecture

If your server entrypoint exports a create function, then Platformatic Node will execute it and then will wait for it to return a server object. In this situation the server will be used without starting a TCP server. The TCP server is started if the application is the runtime entrypoint.

If your server entrypoint does not export a function, then Platformatic runtime will execute the function and wait for a TCP server to be started.

In both cases, the listening port is always modified and chosen randomly, overriding any user or application setting.

If the application uses the commands property then it's always responsible to start a HTTP server and the create functions are not supported anymore.

In all cases, Platformatic runtime will modify the server port replacing it with a random port and then it will integrate the external application in the runtime.

If your application entrypoint exports a create or build function that returns an object with isBackgroundApplication set to true, then Platformatic Node will treat the application as a background application which doesn't expose any HTTP port. If the returned object has a close function, it will be called upon application shutdown as close(app), where app is the returned object.

Alternatively, your application entrypoint can export a hasServer variable set to false, or you can set the node.hasServer property to false in your watt.json file. To gracefully shut down an application with hasServer=false, you may export a close function that will be called upon application shutdown.

HTTPS

When a @platformatic/node application is the Watt entrypoint, configure HTTPS in the runtime server.https object:

{
"$schema": "https://schemas.platformatic.dev/wattpm/3.0.0.json",
"entrypoint": "api",
"server": {
"hostname": "127.0.0.1",
"port": 3042,
"https": {
"key": { "path": "./certs/server.key" },
"cert": { "path": "./certs/server.crt" }
}
},
"applications": [
{
"id": "api",
"path": "./services/api"
}
]
}

Use getAdditionalServerOptions() in your Node.js application and pass the returned options to node:https.createServer():

import { getAdditionalServerOptions } from '@platformatic/globals'
import { createServer } from 'node:https'

const server = createServer(getAdditionalServerOptions(), (req, res) => {
res.end('ok')
})

server.listen(0)

Watt reads key and cert file paths before loading the application, so getAdditionalServerOptions() returns the sanitized TLS options that node:https expects. reuseTcpPorts remains enabled by default and HTTPS servers use SO_REUSEPORT when the current Node.js version and operating system support it.

Example applications entrypoints

Fastify with build function

import { getBasePath, getLogLevel } from '@platformatic/globals'
import fastify from 'fastify'

export function create () {
const app = fastify({
logger: { level: getLogLevel({ throwOnMissing: false }) ?? 'info' }
})

const prefix = getBasePath({ throwOnMissing: false }) ?? ''

app.get(`${prefix}/env`, async () => {
return { production: process.env.NODE_ENV === 'production' }
})

return app
}

Express with no build function

import { getBasePath } from '@platformatic/globals'
import express from 'express'

const app = express()

const prefix = getBasePath({ throwOnMissing: false }) ?? ''

app.get(`${prefix}/env`, (req, res) => {
res.send({ production: process.env.NODE_ENV === 'production' })
})

app.listen(3000)

Background only application

export async function create () {
const id = setInterval(() => console.log('alive'), 10_000)

return {
isBackgroundApplication: true,
id,
async close (app) {
clearInterval(app.id)
}
}
}

Alternatively, for modules that start background work when imported, export hasServer as false:

import { getMessaging } from '@platformatic/globals'

export const hasServer = false

const messaging = getMessaging()
messaging.handle('ping', () => 'pong')

const timeoutId = setTimeout(() => console.log('done'), 10_000)

// Optionally provide a close function
export async function close () {
clearTimeout(timeoutId)
}

Handling Application Shutdown

When your Node.js application needs to gracefully shut down, Platformatic provides several mechanisms to ensure resources are properly cleaned up:

close Function

Export a close function from your application entry point to handle cleanup when the application shuts down:

// Store references to resources that need cleanup
let server
let database

export async function close () {
console.log('Cleaning up...')

// Clean up your resources
if (database) {
await database.close()
}
if (server) {
await server.close()
}

console.log('Application closed gracefully')
}

close Event Handler

Alternatively, you can register a close event handler using the Platformatic events getter. getEvents() returns PlatformaticEvents, an EventEmitter with an additional emitAndNotify(event, ...args) method for emitting locally and notifying the runtime.

import { getEvents } from '@platformatic/globals'

const events = getEvents()
events.on('close', () => {
console.log('Received close event, cleaning up...')

// Perform your cleanup operations
})

Symbol.asyncDispose

If your create or build factory returns an object with a Symbol.asyncDispose method, Platformatic will automatically call it during shutdown:

import { createServer } from 'node:http'

export function create () {
const server = createServer((_, res) => {
res.end('Hello')
})

return {
listen (...args) {
return server.listen(...args)
},
async [Symbol.asyncDispose] () {
// Perform your cleanup operations
await new Promise((resolve, reject) => {
server.close(err => (err ? reject(err) : resolve()))
})
}
}
}

This follows the TC39 Explicit Resource Management convention. The Symbol.asyncDispose method is called after the close event is emitted and before the server is closed.

Fastify Applications

Fastify applications are handled automatically by Platformatic, but custom cleanup logic can still be implemented using the other mechanisms above if needed.

General behavior

Platformatic Node handles closing the main application components:

  • Applications with create function: It will invoke the close method on the server returned by the function.
  • Applications without create function: It will invoke the close method on the first node:http server that listened on a TCP port.

However, additional resources must be manually closed using the mechanisms described above, otherwise the application will hang during shutdown and eventually timeout.

In applications launched via custom commands only the close event handler is available for cleanup and the close function is ignored.

If your application needs to clean up some shared states (connection pool, etc), you must export a close function, handle the close event, or implement Symbol.asyncDispose on the object returned by your factory. If you don't, Platformatic will log a warning message suggesting you implement proper cleanup to avoid exit timeouts. The exception is Fastify.

Typescript

The Platformatic Node allows to run Typescript application with the use of custom commands via the commands property.

To make Typescript work in development mode, setup a commands.development value which will start Node.js with a TypeScript loader.

When configuring production mode instead, you have to configure both the commands.build and commands.production values. The former will be used to compile your application, while the latter will be used to start it.

A complete typical setup for the application watt.json file will be something like this:

{
"$schema": "https://schemas.platformatic.dev/@platformatic/node/2.9.1.json",
"application": {
"commands": {
"development": "node --import tsx server.ts",
"build": "tsc",
"production": "node dist/server.js"
}
}
}

Watt supports setting up npm run ... commands so you can reuse your existing npm scripts flow.

Configuration

See the configuration page.

Runtime APIs

During application execution, Platformatic exposes runtime APIs through typed getters and setters from @platformatic/globals.

import { getApplicationId, getLogger } from '@platformatic/globals'

const applicationId = getApplicationId()
const logger = getLogger()

logger.info({ applicationId }, 'Application started')

Common APIs include:

  • getLogger() to access the application logger.
  • getBasePath() to read the application base path.
  • getMessaging() to exchange messages with other runtime applications.
  • getPrometheus() to register custom metrics in the runtime registry.
  • getEvents() to listen for lifecycle events such as close.
  • setCustomHealthCheck() and setCustomReadinessCheck() to customize health and readiness checks.

Direct access through globalThis.platformatic is still supported for compatibility, but deprecated. Use the typed APIs from @platformatic/globals instead.

See the Runtime APIs reference for the complete API list, error handling behavior, and examples.

Issues

If you run into a bug or have a suggestion for improvement, please raise an issue on GitHub or join our Discord feedback channel.