npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

froge

v2.0.0

Published

Jump-start your NodeJS/Bun/... services with dependency & lifecycle management and handy helper methods

Readme

Froge

Jump-start your NodeJS/Bun/... services with dependency & lifecycle management and handy helper methods.

froge

Froge is Typescript-focused and allows type safe access to services in the context.

npm i froge

Learn more

Basic usage

Froge lets you define service groups which depend on each other.

Calling froge() creates a Froge Server, which can be populated with services:

  • up() defines how to start services
  • down() defines how to stop them
  • use() adds services from another server instance (effectively, a plugin)

Services are stopped in the reverse order.

import froge from 'froge';

const plugin = froge().up({
    externalService: ctx => 'I am a service from another instance',
});

const server = froge()
.up({
    service1: ctx => 'I am service 1',
    service2: async ctx => await new Promise(resolve => resolve('I am service 2')),
})
.up({
    service3: ctx => `I am service 3 and I depend on "${ctx.services.service1}"`,
})
.use(plugin)
.up({
    service4: ctx => `I depend on another instance service "${ctx.services.externalService}"`,
});

server.down({
    service1: service1 => console.log(`I'm stopping "${service1}"`),
    service4: async service4 => await new Promise(resolve => {
        console.log(`I'm stopping "${service4}"`);
        resolve();
    }),
})

server.launch()
    .then(() => console.log('Server is ready'));

Advanced example

Slightly more realistic example demonstrating all available features

import froge from 'froge';

// 3rd party libraries used in an example:
import 'dotenv/config'; // load .env file
import mysql from 'mysql2/promise';
import { Telegraf } from 'telegraf';
import express from 'express';
import type { Server } from 'http';

froge()
    .configure({
        // If started with launch() method, Froge will handle Ctrl+C
        // It's recommended to set timeout to kill the app if it didn't stop on it's own
        gracefulShutdownTimeoutMs: 15000,
        // If you don't want to see console output
        verbose: false,
    })
    // First group of the services
    .up({
        db: ctx => mysql.createPool({
            // Use handy helpers for validating common env var values
            host: ctx.envs.MYSQL_HOST.string('localhost'),
            port: ctx.envs.MYSQL_PORT.port(3306),
            // ...
        }),
        hourlyJoke: async ctx => {
            const fetchJoke = async () => (await fetch(`https://v2.jokeapi.dev/joke/${ctx.envs.JOKE_TOPIC.string('Programming')}?format=txt&type=single`)).text();
            let joke = await fetchJoke();
            let interval = setInterval(async () => {
                joke = await fetchJoke();
            }, 3600000);
            return {
                get joke() {
                    return joke;
                },
                stop: () => clearInterval(interval),
            };
        },
    })
    // Second group of services, which depend on first
    .up({
        api: ctx => {
            const db = ctx.services.db;
            return {
                routes: express.Router()
                    .get('/joke', async (req, res) => {
                        // Access other services
                        res.send(ctx.services.hourlyJoke.joke);
                    })
                    .post('/like', async (req, res) => {
                        await db.query('UPDATE likes SET amount = amount + 1 WHERE joke = ?', [req.query.joke]);
                    }),
            };
        },
        telegram: async ctx => {
            const bot = new Telegraf(ctx.envs.TG_BOT_TOKEN.s() /* s() is short for string() */);
            const webhookRoutes = await bot.createWebhook({domain: ctx.envs.PUBLIC_ADDRESS.string()});
            return { bot, webhookRoutes };
        },
    })
    // Now it's time for http server, which exposes routes from other services above
    .up({
        http: async ctx => {
            let server: Server;
            await new Promise<void>((resolve, reject) => {
                server = express()
                    .use(ctx.services.telegram.webhookRoutes)
                    .use('/api', ctx.services.api.routes)
                    .listen(ctx.envs.LISTEN_PORT.port(8080), err => err ? reject(err) : resolve());
            });
            return server!;
        },
    })
    // Define how to stop the services
    .down({
        db: async pool => await pool.end(),
        hourlyJoke: service => service.stop(),
        telegram: async service => service.bot.stop(),
        http: async service => service.close()
    })
    // .launch() automates lifecycle management, but it can be handled manually using .start()/.stop() instead
    .start()
        .then(froge => {
            console.log("I'm ready!");
            process.once('SIGINT', () => {
                froge.stop().catch(e => {
                    console.error('Failed to stop: ', e);
                    process.exit(1);
                });
            });
        })
        .catch(e => {
            console.error('Failed to start: ', e);
            process.exit(1);
        });

Start one specific service

only method starts a specific service and all it's dependencies. It can be useful to write cli commands for your server.

Imagine a server which has a db service and some others, defined in server.ts:

import froge from 'froge';
import { createPool } from 'mysql2/promise';

export default froge()
    .up({ /* dependencies of db (imagine something here), will be started */ })
    .up({
        // db - will be started
        db: ctx => createPool({
            host: ctx.envs.MYSQL_HOST.s('localhost'),
            port: ctx.envs.MYSQL_PORT.port(3306),
            // ...
        }),
        something: () => 'something else', // won't start
    })
    .up({ /* more services that won't start */ })
    .down({
        db: pool => pool.end(),
    })

You only need to start db in the cli command migrate:

import { Command } from 'commander';
import server from './server';

const program = new Command();
program.command('migrate')
    .description('Init database structure')
    .action(async () => {
        const db = await server.only('db'); // this will only start db and it's dependencies
        try {
            await db.query('CREATE TABLE ...');
        } finally {
            await server.shutdown();
        }
    });

program.parse();

Inferred context

When service has lots of dependencies, you may want to pass the context as is to the service instead.

This is a more invasive approach as your services will have to know about the froge context, but it can be handy sometimes.

server.ts

import froge, { type InferContext } from "froge";
import { TestService } from "./test-service";

export const server = froge().up({
    test1: () => 'test1',
    test2: () => 'test2',
}, 'alpha' /* <== */).up({
    testService: ctx => new TestService(ctx),
}, 'beta');

// Contains all services from the first group named "alpha"
export type AlphaContext = InferContext<typeof server, 'alpha' /* <== */>;

// Contains all services from the second group named "beta"
export type BetaContext = InferContext<typeof server, 'beta'>;

test-service.ts

import type { AlphaContext } from "./server";

export class TestService {
    constructor(private ctx: AlphaContext) {}

    public test() {
        // Services from the first group available
        return this.ctx.services.test1 + '+' + this.ctx.services.test2;
    }
}

Reverse dependencies (service plugs)

Sometimes the service may need to communicate with a service in the group below.

There are two ways to implement this.

Event emitter:

import froge from "froge";
import EventEmitter from 'events';

const server = froge().up({
    events: () => new EventEmitter(),
}).up({
    service1: ctx => ({
        // there is no service2 in the context, but we can send an event
        sendFoo: () => ctx.services.events.push('foo', 'bar'),
    }),
}).up({
    service2: ctx => {
        ctx.services.events.on('foo', data => console.log(data));
    },
});
await server.launch();
server.services.service1.sendFoo(); // prints "bar"

Service plugs:

A more complex method would be to add a plug for a service, which itself will be added later.

import froge from "froge";

const server = froge().up({
    service2: ctx => ctx.plug<{
        acceptFoo: (data: string) => void,
    }>(),
}).up({
    service1: ctx => ({
        // there is a plug for service2 in the context, with acceptFoo method available
        sendFoo: () => {
            // It must not be accessed before actual service2 started, it will cause an error
            if (!ctx.service.service2.isReady) {
                console.log('service2 not ready yet');
            } else {
                // Note! service2 is called as a function to access the service
                ctx.services.service2().acceptFoo('bar');
            }
        },
    }),
}).up({
    // Normally, existing service can't be overwritten (unless it's a plug)
    // Type declaration must be compatible with a plug defined above (extra properties are allowed)
    service2: ctx => {
        const myService = {
            acceptFoo: (data: string) => console.log(data),
            somethingElse: () => console.log('Something else!'),
        };
        // Note! Plug services must return a function
        return () => myService;
    },
});
await server.launch();
server.services.service1.sendFoo(); // prints "bar"
server.services.service2().somethingElse();

Plugins and plugin development

Froge server use() method can be used for code organisation (split server into parts), but it is also useful to develop plugins.

A callback can be passed to use() method, which allows to construct the plugin while having access to services in the main instance.

Below is an example of a simple ExpressJS plugin:

import type { Server } from 'http';

function createExpressServer(app: any, port: number) {
    return froge.up({
        http: async ctx => {
            let server: Server;
            await new Promise<void>((resolve, reject) => {
                server = app.listen(port, err => err ? reject(err) : resolve());
            });
            return server!;
        },
    }).down({
        http: async service => service.close(),
    });
}

And how it can be used:

import froge from 'froge';
import mysql from 'mysql2/promise';
import express from 'express';

const server = froge().up({
    db: ctx => mysql.createPool({
        host: ctx.envs.MYSQL_HOST.string('localhost'),
        port: ctx.envs.MYSQL_PORT.port(3306),
        // ...
    }),
}).use(ctx => {
    // DB pool from a previous step
    const db = ctx.services.db;
    // Normal ExpressJS app
    const app = express()
        .post('/increment', async (req, res) => {
            await db.query('UPDATE counts SET amount = amount + 1 WHERE key = ?', [req.query.key]);
        });
    // Create the plugin (server, which will start/stop express for us)
    return createExpressServer(app, ctx.envs.LISTEN_PORT.port(8080));
}).down({
    db: pool => pool.end(),
});

await server.launch();

Full configuration reference

interface FrogeConfig {
    /** Start services which don't depend on each other in parallel */
    parallelStartGroups: boolean,
    /** Stop services which don't depend on each other in parallel */
    parallelStopGroups: boolean,
    /** Kill the process if shutdown took longer than expected */
    gracefulShutdownTimeoutMs?: number,
    /** Force exit the current process after shutdown is completed */
    forceExitAfterShutdown: boolean,
    /** Print info logs */
    verbose: boolean,
}