Weenie

A Typescript Microframework by Wymp

Quickstart

(For a really quick start, Explore the example app to see a full, working Weenie app built in the Weenie way.)

Install the Weenie framework from npm: npm install @wymp/weenie-framework.

Most frameworks (such as NestJs) ship with a fully-baked "way" of doing things. This is fine... until it's not. And then it can be maddening.

Weenie is a framework that ships with a way of doing things, but helps you capture and re-use your own way of doing things and use that when you prefer it.

Don't like how Weenie does logging? Swap in your own logger. Don't like how Weenie does pubsub? Swap in someone else's.

The point is that opinions are powerful and necessary, but your code should reflect your opinions, and most frameworks make that difficult.

Here's a quick look at what Weenie looks like:

// src/deps.ts
import { Weenie, logger, cron, mysql, express } from '@wymp/weenie-framework';
import { getConfig } from './myConfig';

// Note that you can define your config however you'd like - this is not an important
// detail of the example. However, Weenie ships with the `@wymp/config-simple` package,
// which is a simple and recommended way of managing configuration.
//
// Whatever you choose, it's important to note that your config will usually be used by
// subsequent dependencies, and you may need to define a config adapter or something if
// you want to use a standard Weenie function with a different config system.
export const getDeps = () => Weenie({ config: getConfig() })
  .and(logger)
  .and(cron)
  .and(mysql)
  .and(express)
  .done((d) => d);

export type Deps = ReturnType<typeof getDeps>;



// src/main.ts
// This is the entrypoint to your app
import { getDeps } from './deps';
import { Handlers } from './http/handlers';
import { CronJobs } from './cron/jobs';

// Get your deps
const deps = getDeps();

// Log some stuff
deps.log.debug(`Registering cronjobs and HTTP endpoints...`);

// Set up some HTTP endpoints
deps.http.get('/', Handlers.root(deps));
deps.http.get('/users', Handlers.users(deps));

// Set up some cronjobs
deps.cron.register(CronJobs.doSomething(deps));

// Start the server listening
deps.http.listen((listener) => {
  deps.log.notice(`Server listening on ${listener[1] ?? 'localhost'}:${listener[0]}`);
});
      

Deeper Dive

Strictly speaking, Weenie is just a function with a few little type tricks that presents a way of creating a dependency injection container. And DI is simply a way of building clean, portable code. Take a simple "create user" example function:

// src/lib/users.ts
// TRADITIONAL WAY - Dependencies imported and used directly, side effects all over the place
import { UserModel, type User } from './models/user';

export const createUser = async (input: User) => {
  const newUser = UserModel.create(input);
  await newUser.save();
  console.log(`New user created: ${newUser.id}`);
  return newUser;
}
      

Simple, but there are several issues with this. First, you can't import this code unless you have an active configured database connection, since the UserModel is a direct import from an ORM. And second, whenever you run this code, it will call console.log and there's nothing you can do about it.

These don't seem like big issues on face value, but they're a style that often leads to tech debt.

With Weenie, you would simply rewrite your function like this:

// src/lib/users.ts
// WEENIE WAY - Dependencies are passed in, no side effects
import type { Deps } from '../deps';
import type { User } from './models/user';

export const createUser = async (input: User, deps: Pick<Deps, 'db' | 'log'>) => {
  const newUser = await deps.db.users.create(input);
  deps.log.info(`New user created: ${newUser.id}`);
  return newUser;
}
      

This is basically the same code, but now we're injecting our dependencies, which means we can very easily test and debug the code. For example, we can import this file in a node REPL and pass in mock dependencies and custom input to probe its functionality interactively. Or we can create a mock data layer and pass that in via unit tests to easily test the function's behavior.

Also important to note is that we're now depending on the _type_ of our dependencies, rather than a concrete implementation. This makes it easier to swap things out later and/or use this code in different contexts.

Give it a Try

Weenie is a framework that helps you build your own framework. It's a way of doing things that helps you capture and re-use your own way of doing things. It's a way of writing clean, portable code that's easy to test and debug, and that reflects the true personalities of the people who write it.

Explore the Example App or check out the API docs to learn more about Weenie and how to code your own way.