Build Your First Plugin
In this tutorial you'll build a Weather plugin that exposes a REST endpoint to fetch current weather data for any location. Estimated time: 15 minutes.
Scaffold the project
Use the official generator to create a typed plugin skeleton.
Configure TypeScript
Review package.json and tsconfig.json generated by the scaffolder.
Implement the plugin
Write the plugin entry point with lifecycle hooks and a custom route.
Write tests
Use the built-in test utilities to unit-test your routes.
Run locally
Hot-reload your plugin into a running UniCore dev instance.
Publish
Build and publish your plugin to npm for others to install.
Scaffold the project
Run the official scaffold generator. It creates a ready-to-use TypeScript project with all boilerplate pre-configured:
# Scaffold a new plugin project
npx @bemindlabs/unicore-create-plugin my-weather-plugin
cd my-weather-pluginPlugin package names must start with unicore-plugin- so UniCore can discover them automatically.
Review the configuration
The scaffolder generates these files. Review and adjust as needed:
{
"name": "unicore-plugin-weather",
"version": "1.0.0",
"description": "Show weather data inside UniCore",
"main": "dist/index.js",
"unicorePlugin": true,
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "jest"
},
"dependencies": {
"@bemindlabs/unicore-plugin-sdk": "^1.0.0"
},
"devDependencies": {
"typescript": "^5.5.0",
"@types/node": "^20.0.0",
"jest": "^29.0.0"
}
}{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}The "unicorePlugin": truefield in package.json is required for UniCore's plugin discovery to recognise your package.
Implement the plugin
Open src/index.ts and replace the generated boilerplate:
import { definePlugin } from '@bemindlabs/unicore-plugin-sdk'
const WEATHER_API = 'https://api.open-meteo.com/v1/forecast'
export default definePlugin({
id: 'my-weather-plugin',
name: 'Weather',
version: '1.0.0',
description: 'Fetch current weather for any city.',
async onActivate(ctx) {
ctx.log.info('Weather plugin activated')
// Register a REST route: GET /api/plugins/my-weather-plugin/weather?lat=&lon=
ctx.router.get('/weather', async (req, res) => {
const { lat, lon } = req.query as { lat: string; lon: string }
if (!lat || !lon) {
return res.status(400).json({ error: 'lat and lon are required' })
}
const url = new URL(WEATHER_API)
url.searchParams.set('latitude', lat)
url.searchParams.set('longitude', lon)
url.searchParams.set('current_weather', 'true')
const response = await fetch(url.toString())
const data = await response.json()
return res.json({
temperature: data.current_weather.temperature,
windspeed: data.current_weather.windspeed,
weathercode: data.current_weather.weathercode,
})
})
},
async onDeactivate(ctx) {
ctx.log.info('Weather plugin deactivated')
},
})Key points:
- ctx.router.get() registers a route at /api/plugins/my-weather-plugin/weather
- Route handlers receive standard Express-compatible req/res objects
- The platform JWT middleware protects all plugin routes automatically
- Use ctx.log instead of console.log — logs appear in the admin UI
Write tests
The SDK ships with a testing sub-package that provides an in-memory PluginContext so you can unit-test plugin logic without a running UniCore instance:
import { createTestContext } from '@bemindlabs/unicore-plugin-sdk/testing'
import plugin from '../src/index'
describe('Weather plugin', () => {
it('registers GET /weather route', async () => {
const ctx = createTestContext({ pluginId: plugin.id })
await plugin.onActivate!(ctx)
const routes = ctx.router.getRegisteredRoutes()
expect(routes).toContainEqual({ method: 'GET', path: '/weather' })
})
it('returns 400 when lat/lon missing', async () => {
const ctx = createTestContext({ pluginId: plugin.id })
await plugin.onActivate!(ctx)
const { status, body } = await ctx.router.inject('GET', '/weather')
expect(status).toBe(400)
expect(body.error).toBe('lat and lon are required')
})
})Run locally
Point your local UniCore dev instance to your plugin directory using theUNICORE_LOCAL_PLUGINS env var. Changes are hot-reloaded automatically:
# In your plugin directory
pnpm dev
# In another terminal, tell UniCore to load your local plugin
export UNICORE_LOCAL_PLUGINS=/path/to/my-weather-plugin
docker compose --profile apps restart unicore-api-gatewayThen test your route:
# Test the route via the API gateway (requires JWT)
curl http://localhost:4000/api/plugins/my-weather-plugin/weather?lat=51.5&lon=-0.12 \
-H "Authorization: Bearer ${TOKEN}"Publish
Build your plugin and publish it to npm. Any UniCore instance can then install it by running npm install unicore-plugin-weather:
# Build for production
pnpm build
# Publish to npm (must match unicore-plugin-* naming)
npm publish --access public