DEV Community

Cover image for Building Our Own `create-app` CLI — A Custom NPX Script for Fast Project Bootstrapping
Arjun
Arjun

Posted on

Building Our Own `create-app` CLI — A Custom NPX Script for Fast Project Bootstrapping

Intro

When our team began building multiple web applications using Next.js, we didn’t initially think about creating a CLI tool. We just wanted to ship products fast and maintain a consistent development experience across projects. But as the number of products grew, we realized that setting up each new application from scratch was repetitive, time-consuming, and error-prone.

This is the story of how we built our own custom npx script, inspired by tools like create-next-app, to help our developers spin up new projects instantly — complete with our in-house configurations, libraries, and folder structure.


The Beginning: From One App to Many

It started with one app.

Then came another.

And soon, several more.

Our frontend team was responsible for building multiple product UIs for different internal and customer-facing tools. We were using Next.js as our primary framework, following the Feature-Sliced Design (FSD) methodology — an architectural pattern that helps organize frontend projects into clear, maintainable layers.

Each new app required:

  • Installing common dependencies (like our in-house component and utility libraries).
  • Setting up authentication logic.
  • Maintaining a consistent folder structure (based on FSD).
  • Applying our own ESLint, Prettier, and Husky configurations.
  • Enforcing a consistent code style and commit rules across repositories.

After a few projects, we realized we were spending too much time doing setup work that could easily be automated.


The Starter Template

So we created a starter template — a cleaned-up, minimal Next.js app that included all our standard configurations and base dependencies.

It served as a “bare minimum” project — something new developers could clone and start coding right away. The template included:

  • ✅ Next.js setup with TypeScript
  • ✅ Pre-configured ESLint, Prettier, and Husky
  • ✅ Authentication boilerplate
  • ✅ FSD folder structure (src/app, src/entities, src/features, etc.)
  • ✅ In-house component and utility libraries
  • ✅ Git hooks for linting and formatting

This helped us a lot initially. But soon, as more developers joined and new types of projects emerged (like prototype apps to test UI designs quickly), managing multiple starter templates manually became difficult.

That’s when we decided to take it one step further.


The Idea: A Custom npx Script Like create-next-app

We wanted to give our developers a simple command like:

npx create-our-app my-new-project --template=starter
Enter fullscreen mode Exit fullscreen mode

or for prototype apps:

npx create-our-app my-proto-project --template=proto
Enter fullscreen mode Exit fullscreen mode

The goal was to automate:

  1. Creating a new project directory.
  2. Copying the correct template files.
  3. Installing dependencies.
  4. Setting up git, linting, and formatting.
  5. Applying our team’s coding standards automatically.

Essentially, we wanted something that felt like create-next-app, but fully tailored to our ecosystem.


The Technical Implementation

We took inspiration from Vercel’s create-next-app repository. It’s a great reference for building custom NPX tools, and its structure made a lot of sense.

Our project followed a similar layout:

create-our-app/
├── templates/
│   ├── starter/
│   ├── proto/
│   └── common/
├── src/
│   ├── helpers/
│   │   ├── downloadTemplate.ts
│   │   ├── copyTemplateFiles.ts
│   │   ├── installDependencies.ts
│   │   └── log.ts
│   └── index.ts
├── package.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

The index.ts file serves as the entry point — it handles argument parsing, project name validation, and calls helper functions for each setup step.

Setting Up package.json for a CLI Tool

To make your CLI runnable with npx or after a global install, you need to configure the bin field in your package.json.
This field tells Node.js which file to execute when the command is called.

Example:

{
  "name": "create-our-app",
  "version": "1.0.0",
  "description": "A custom NPX script to bootstrap frontend projects with our starter templates",
  "bin": {
    "create-our-app": "dist/index.js"
  },
  "type": "module",
  "files": [
    "dist"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/our-org/create-our-app.git"
  },
  "author": "Your Team Name",
  "license": "MIT",
  "dependencies": {
    "commander": "^11.0.0",
    "inquirer": "^9.0.0",
    "fs-extra": "^11.1.1",
    "tar": "^6.2.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • bin — maps your command name (create-our-app) to the executable file (after it’s built, e.g., dist/index.js).
  • type — ensures Node treats your project as an ES module (if using ES imports).
  • files — limits which files are published to npm (here, only dist).
  • repository — links the CLI’s GitHub repo for transparency and easy access.

Once this is set up and you’ve built your project, you can test it locally with:

npm link
Enter fullscreen mode Exit fullscreen mode

Then you can run your CLI globally using:

create-our-app my-app

Argument Parsing

We used the commander library for parsing CLI arguments. It made it easy to handle flags like --template, --use-npm, or --git.

import { Command } from "commander";
const program = new Command();

program
  .name("create-our-app")
  .description("Initialize a new frontend project with our custom setup")
  .option("-t, --template <template>", "Choose a template", "starter")
  .option("--use-npm", "Use npm instead of yarn")
  .parse(process.argv);
Enter fullscreen mode Exit fullscreen mode

Interactive Prompts with Inquirer

We also integrated inquirer to make the CLI interactive.

If a user skips the CLI arguments (like template name or package manager), the script automatically prompts them with questions.

Example:

import inquirer from "inquirer";

const answers = await inquirer.prompt([
  {
    type: "list",
    name: "template",
    message: "Choose a project template:",
    choices: ["starter", "proto"],
    default: "starter",
  },
  {
    type: "list",
    name: "pkgManager",
    message: "Select a package manager:",
    choices: ["npm", "yarn", "pnpm"],
    default: "npm",
  },
]);
Enter fullscreen mode Exit fullscreen mode

Downloading Templates

For flexibility, we wanted developers to pass a GitHub URL instead of just using built-in templates.

For example:

npx create-our-app my-app --template=https://github.com/our-org/custom-template
Enter fullscreen mode Exit fullscreen mode

To achieve that, we used tarball downloads from GitHub’s codeload API:

const url = `https://codeload.github.com/${username}/${repo}/tar.gz/${branch}`;
Enter fullscreen mode Exit fullscreen mode

We then extracted the tar file using the tar package, which let us clone only the needed files instead of pulling the entire Git history.

Copying Local Templates

If a local template was used, we relied on fs-extra for copying directories recursively. This library also helped us handle file overwrites and cleanups safely.

import fs from "fs-extra";

await fs.copy(
  path.join(__dirname, `../templates/${template}`),
  targetDirectory
);
Enter fullscreen mode Exit fullscreen mode

Installing Dependencies

After copying the template, the script automatically installed dependencies using either npm, yarn, or pnpm — depending on what the developer preferred or had installed.

import { execSync } from "child_process";

execSync(`${pkgManager} install`, { stdio: "inherit" });
Enter fullscreen mode Exit fullscreen mode

The Result: Developer Happiness 😄

After introducing our custom CLI, onboarding new developers became seamless.

Instead of sharing long setup instructions, we just gave them a single command. Within a couple of minutes, they had a fully configured Next.js project with all our defaults in place.

Some key benefits we noticed:

  • Faster onboarding — No more copy-pasting config files.
  • 🔒 Consistency — Every project follows the same structure and rules.
  • 🧩 Scalability — Adding new templates (e.g., mobile dashboard, prototype app) is easy.
  • 🔧 Maintainability — Centralized control over shared tooling and updates.

Lessons Learned

A few things we learned while building this:

  • Start simple — the first version doesn’t need every feature.
  • Document every flag and template — new developers will thank you later.
  • Avoid making assumptions about the environment (e.g., which package manager is installed).
  • Use meaningful logs and color-coded output for better CLI UX (we used chalk for this).
  • Always clean up temporary files and handle errors gracefully.

What’s Next

We’re planning to add:

  • More interactive prompts for optional modules (like auth, analytics, or feature toggles).
  • Remote template versioning for better maintainability.

The journey of building this tool taught us that developer experience (DX) is as important as user experience. By investing a bit of time in tooling, we saved countless hours for our team and made development far more consistent and enjoyable.


In short:

Our custom npx script started as a small internal experiment but quickly became one of the most impactful productivity tools in our development workflow.

Top comments (0)