@dunx/create-app
Scaffold a new dunx application - bunx @dunx/create-app my-api
Scaffolds a new dunx application.
bunx @dunx/create-app my-api
bun create dunx-appdoes not work and is not advertised:bun create <template>resolves the unscoped npm packagecreate-<template>, which this package - being scoped - is not.
cd my-api
bun install
bun run dev # http://localhost:3000/greetings, restarting on a changeThe 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.| Key | Does |
|---|---|
↑ ↓, k j, Tab | Move the cursor |
| Space | Toggle the feature under it |
a / n | Everything / nothing |
| Enter | Take the selection |
| Ctrl+C, Esc | Stop, 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
| Flag | Default | Meaning |
|---|---|---|
--name <name> | the directory name | Package name for the generated app |
--force | off | Write into a directory that already has files |
--yes, -y | off | Skip the questions, take the minimal template |
--help | Print 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
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 preloadThree 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
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.