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
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:
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:
tsc && node dist/lib/schema.js > schemas/acme.json
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.