@dunx/create-app

Scaffold a new dunx application - bunx @dunx/create-app my-api

@dunx/create-app

Scaffolds a new dunx application.

SH
bunx @dunx/create-app my-api

bun create dunx-app does not work and is not advertised: bun create <template> resolves the unscoped npm package create-<template>, which this package - being scoped - is not.

cd my-api
bun install
bun run dev       # http://localhost:3000/greetings, restarting on a change

The questions

There is no flag for choosing features. The command opens a list:

? Features  2 chosen, 1 pulled in
  ○ notes       CRUD routes with zod validation. The smallest real feature.
  ◉ openapi     OpenAPI 3.1 from the routes own schemas, plus the Swagger UI page.
  ◈ database    drizzle over bun:sqlite, with a schema, seeds and migrations.
❯ ◉ users       A repository, a service and validated routes over the database.
  ○ auth        better-auth mounted, with SessionGuard and an audit trail.
  database comes along as a requirement.
  Space toggles. ↑↓ moves. a all, n none. Enter continues.
KeyDoes
↑ ↓, k j, TabMove the cursor
SpaceToggle the feature under it
a / nEverything / nothing
EnterTake the selection
Ctrl+C, EscStop, having written nothing

◉ is chosen, ◈ is pulled in by something else you chose, ○ is neither. The two lines under the list update as you go: one shows what your selection drags in, the other shows which of it needs Redis or Postgres running to do anything.

After the list, you are asked whether to compile the app to one standalone binary. Yes adds scripts/build.ts and a build script. bun run build then uses bun build --compile to write one executable that runs on a host with nothing installed.

Three more questions appear only when there is something to ask: a directory, when the command line named none; a package name, when the directory's is one npm would reject; and whether to write into a directory that already has files in it.

Options

FlagDefaultMeaning
--name <name>the directory namePackage name for the generated app
--forceoffWrite into a directory that already has files
--yes, -yoffSkip the questions, take the minimal template
--helpPrint usage

Piped, redirected or in CI it asks nothing. It writes the minimal template instead, so a script never hangs on a question nothing can answer. To choose features without a terminal, call scaffold rather than passing flags.

The name is validated against npm's rules before anything is created, because an invalid one would otherwise surface as a confusing bun install failure inside a directory you just made.

Scaffolding into a repo you already made

SH
mkdir my-api && cd my-api && git init
bunx @dunx/create-app .

.git, .gitkeep, .DS_Store and LICENSE are ignored when checking whether the directory is empty, so a new repo or a clone of an empty GitHub repository works without a prompt. Any other file counts. The template includes .gitignore and README.md, and the last question asks whether to overwrite yours.

What a composed app looks like

my-api/
  src/
    main.ts          exports createApp, and serves it when run directly
    app.module.ts    the root module, importing every feature
    config.ts        one validation function, flat env in and a shaped object out
    users/           one directory per feature you chose
  package.json       dev, start, test, typecheck
  bunfig.toml        the transform preload

Three files are generated for the selection, and the feature directories are just copied. main.ts is one file rather than two: a test imports createApp from it, and the import.meta.main block at the bottom stops that same import from starting a server.

There is no worker entry point, even with queues. QueueModule is given consume: true, so the container opens the bullmq workers at onInit and closes them before the connections the handlers use. A handler marked background: true is forked by bullmq itself into src/jobs/jobs.processor.ts.

What it generates

The minimal template is the same app as examples/minimal: a service, a controller, a module, HttpFactory.create, one test against a real server, and the bunfig.toml preload line that makes constructor injection work.

Its src/ is a byte-for-byte copy of that example. A test in this package fails if the two ever drift.

Every app also gets an AGENTS.md naming its layout, its commands and the rules dunx fails at boot over, plus a CLAUDE.md pointing at it. Both link https://dunx.win/setup.md, which is served per release, rather than copying the framework's own instructions into your repository.

The example is the one CI boots, so keeping them identical makes the template trustworthy rather than merely plausible.

Two details

Versions are resolved at run time rather than written into the template. Every @dunx/* range in the template manifest is __DUNX_VERSION__, replaced with a caret range on this package's own version. dunx versions in lockstep - every package shares one number and ships together - so the version doing the scaffolding is by definition a set that works together.

Writing versions into the template would go stale on the next release.

When you add a @dunx/* package later, use the same version as the others. The packages peer-depend on each other by caret range, so mixing minor versions warns on install. It can also install two copies of @dunx/core, which breaks dependency injection: a token is a class, so a provider bound with one copy's class cannot be found through the other copy's.

The template's .gitignore ships as _gitignore. npm renames a published .gitignore to .npmignore, which would leave every scaffolded app without one. The scaffolder puts the dot back on write.

Programmatic use

TS
import { scaffold } from '@dunx/create-app';

const { directory, files } = await scaffold({
  target: 'my-api',
  name: '@acme/my-api',
  features: ['users', 'openapi'],
});

This is the scripted path the removed --with flag used to be. features takes the same names the list shows, in any order, and pulls in what they require. FEATURES exports the set. Omitting it writes the minimal template. binary: true adds the standalone-binary build, and forces the generation path so it works with no features too.

scaffold throws ScaffoldError for anything the caller can fix - an unknown template, an unusable package name, a non-empty target without force - and lets everything else propagate.