Skip to main content
Version: 3.0.6

Programmatic API

In many cases, it's useful to start Platformatic Service using an API instead of the command line, e.g., in tests where we want to start and stop our server programmatically.

Using create Function

The create function allows starting the Platformatic Service programmatically.

Basic Example

server.js
import { create } from '@platformatic/service'

const app = await create('path/to/platformatic.service.json')

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

Custom Configuration

It is also possible to customize the configuration:

import { create } from '@platformatic/service'

const app = await create('path/to', {
server: {
hostname: '127.0.0.1',
port: 0
}
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

Creating a Reusable Application on Top of Platformatic Service

Platformatic DB is built on top of Platformatic Service. If you want to build a similar kind of tool, follow this example:

Example Plugin
import { create, schema as serviceSchema, transform as serviceTransform } from '@platformatic/service'
import { readFileSync } from 'node:fs'

async function myPlugin (app, capability) {
await platformaticService(app, capability)

await app.register(platformaticService, opts)
}

// break Fastify encapsulation
myPlugin[Symbol.for('skip-override')] = true

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
const schema = { ...serviceSchema }

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
async function transform (config, schema, options) {
config = await serviceTransform(config, schema, options)

// do something

return config
}

const server = await create('path/to/config.json', null, { schema, applicationFactory: myPlugin, transform })

await server.start()

const res = await fetch(server.listeningOrigin)
console.log(await res.json())

// do something

await server.close()

TypeScript Support

To ensure this module works in a TypeScript setup (outside an application created with npm create wattpm), you need to import types from @platformatic/service.

Type Declarations

import { ServerInstance } from '@platformatic/service'

export default async function myPlugin (app: ServerInstance) {
app.get('/', async () => {
return app.platformatic.config
})
}

Usage with Custom Configuration

If you are creating a reusable application on top of Platformatic Service, you would need to create the types for your schema, using json-schema-to-typescript in a ./config.d.ts file and use it like this:

Custom Configuration Types

import { ServerInstance } from '@platformatic/service'
import { YourApp } from './config'

export default async function myPlugin (app: ServerInstance<YourApp>) {
app.get('/', async () => {
return app.platformatic.config
})
}

Writing a Custom Capability with TypeScript

Creating a reusable application with TypeScript requires a bit of setup. First, create a schema.ts file that generates the JSON Schema for your application.

Schema Definition

import { schema as serviceSchema } from '@platformatic/service'

export const schema = structuredClone(serviceSchema)

schema.$id = 'https://raw.githubusercontent.com/platformatic/acme-base/main/schemas/1.json'
schema.title = 'Acme Base'

// Needed to specify the extended module
schema.properties.extends = {
type: 'string'
}

schema.properties.dynamite = {
anyOf: [
{
type: 'boolean'
},
{
type: 'string'
}
],
description: 'Enable /dynamite route'
}

delete schema.properties.plugins

/* c8 ignore next 3 */
if (process.argv[1] === import.meta.filename) {
console.log(JSON.stringify(schema, null, 2))
}

Generate Matching Types

Use json-schema-to-typescript to generate types:

  1. tsc && node dist/lib/schema.js > schemas/acme.json
  2. json2ts < schemas/acme.json > src/lib/config.d.ts

Finally, you can write the actual reusable application:

import { type FastifyInstance } from 'fastify'
import { lstat } from 'node:fs/promises'
import { kMetadata, RawConfiguration, ConfigurationOptions } from '@platformatic/foundation'
import { resolve } from 'node:path'
import {
create as createService,
platformaticService,
ServerInstance,
type PlatformaticServiceConfig as ServiceConfig,
ServiceCapability,
transform as serviceTransform
} from '@platformatic/service'

import { type AcmeBaseConfig } from './config.js'
import dynamite from './dynamite.js'
import { schema } from './schema.js'

export { schema } from './schema.js'

async function isDirectory (path: string) {
try {
return (await lstat(path)).isDirectory()
} catch {
return false
}
}

export default async function acmeBase (
app: ServerInstance<ServiceConfig & AcmeBaseConfig>,
capability: ServiceCapability
) {
if (app.platformatic.config.dynamite) {
app.register(dynamite)
}

await platformaticService(app, capability)
}

Object.assign(acmeBase, { [Symbol.for('skip-override')]: true })

export async function transform (config: ServiceConfig & AcmeBaseConfig): ServiceConfig & AcmeBaseConfig {
// Call the transformConfig method from the base capability
config = await serviceTransform(config)

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.

const paths = []

const pluginsDir = resolve(config[kMetadata].root, 'plugins')

if (await isDirectory(pluginsDir)) {
paths.push({
path: pluginsDir,
encapsulate: false
})
}

const routesDir = resolve(config[kMetadata].root, 'routes')

if (await isDirectory(routesDir)) {
paths.push({
path: routesDir
})
}

config.plugins = { paths }

if (!config.service?.openapi) {
if (typeof config.service !== 'object') {
config.service = {}
}

config.service.openapi = {
info: {
title: 'Acme Microservice',
description: 'A microservice for Acme Inc.',
version: '1.0.0'
}
}
}

return config
}

export async function create (
configOrRoot: string | RawConfiguration,
sourceOrConfig: string | RawConfiguration,
context: ConfigurationOptions
) {
return createService(configOrRoot, sourceOrConfig, { schema, applicationFactory: acmeBase, transform, ...context })
}

Implementing Auto-Upgrade of the Configuration

Platformatic support auto-upgrading the configuration of your capability to the latest version. This enables the use of compatibility options to turn on and off individual features. Imagine that you want to change the default behavior of your capability: you can add a configuration option to set the previous behavior. Then during the upgrade logic, you only have to add this new configuration.

The key to implement this logic is semgrator. semgrator run migrations code based on semantic version rules. So on a breaking/behavior change that results in a new compatibility option in your configuration file, you can add a new migration rule that set the new option to false automatically.

Writing migrations

export const migration = {
version: '1.0.0',
up: (input) => {
// Do something with Config
return input
},
}

Wiring it to the capability

Pass a upgrade function to your create method.

import { abstractLogger } from '@platformatic/foundation'
import { resolve } from 'node:path'
import { semgrator } from 'semgrator'

async function acmeBase (app, capability) {
// ...
}

Object.assign(acmeBase, { [Symbol.for('skip-override')]: true })

async function transform (config, schema, options) {
// ...
}

async function upgrade (logger, config, version) {
const iterator = semgrator({
version,
path: resolve(import.meta.dirname, 'versions'),
input: config,
logger: logger?.child({ name: 'your-app' }) ?? abstractLogger
})

let result

for await (const updated of iterator) {
result = updated.result
}

return result
}

export async function create (configOrRoot, sourceOrConfig, context) {
return createService(configOrRoot, sourceOrConfig, {
applicationFactory: acmeBase,
transform,
upgrade,
...context
})
}

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.