Best Practices for Using Mise to Maintain Project Structure and Manage Environment Variables #
Summary #
Mise (pronounced "meez") is a polyglot tool version manager, environment variable manager, and task runner all in one. Written in Rust, it's the modern replacement for the combination of asdf + direnv + Make. As of 2024, mise has matured beyond its origins as an "asdf rewrite" into a complete development environment manager.
For this Bun + Biome TypeScript project, mise provides the ideal way to: (1) ensure consistent Bun versions across team members, (2) manage environment variables without separate dotenv tools, and (3) define project tasks in a single configuration file.
The key insight is that mise consolidates multiple tools into one: it replaces nvm/asdf for version management, direnv for environment variables, and partially replaces Make/npm scripts for task running—all with blazing fast performance (shell startup in ~117-276 microseconds vs nvm's ~220ms).
Project Context #
This project is planned to use:
- Bun - JavaScript runtime, package manager, bundler, test runner
- Biome - Linting and formatting
- TypeScript - Type checking
Mise can manage Bun's version and integrate with the existing toolchain to provide a complete development environment setup.
Detailed Findings #
Mise Configuration Structure #
What it is: Mise uses .mise.toml (or mise.toml) files to define tool versions, environment variables, and tasks in a single configuration.
Why consider it:
- One file replaces
.nvmrc,.tool-versions,.env,Makefile - Hierarchical configuration allows global, directory, and project-specific settings
- Full asdf plugin compatibility while being 10x faster
Step-by-Step Setup #
1. Install Mise #
1# Install mise
2curl https://mise.run | sh
3
4# Add to your shell (~/.zshrc or ~/.bashrc)
5eval "$(~/.local/bin/mise activate zsh)" # or bash
6
7# Verify installation
8mise --version
2. Create Project Configuration #
Create .mise.toml in your project root:
1# .mise.toml
2# Minimum mise version for this project
3min_version = "2024.11.0"
4
5[tools]
6# Pin Bun to a specific version for consistency
7bun = "1.1"
8
9[env]
10# Project-level environment variables
11NODE_ENV = "development"
12
13# Use templates for dynamic paths
14PROJECT_ROOT = "{{ config_root }}"
15
16# Add local binaries to PATH
17_.path = ["./node_modules/.bin"]
18
19[tasks.dev]
20description = "Start development server with watch mode"
21run = "bun run --watch src/index.ts"
22
23[tasks.test]
24description = "Run tests"
25run = "bun test"
26
27[tasks.lint]
28description = "Lint and format code"
29run = "bunx --bun biome check --write ./src"
30
31[tasks.typecheck]
32description = "Run TypeScript type checking"
33run = "bunx --bun tsc --noEmit"
34
35[tasks.check]
36description = "Run all checks (lint + typecheck + test)"
37depends = ["lint", "typecheck", "test"]
3. Create Local Configuration for Secrets #
Create .mise.local.toml for machine-specific or sensitive settings:
1# .mise.local.toml - DO NOT COMMIT
2[env]
3# API keys and secrets
4API_KEY = "your-secret-key-here"
5DATABASE_URL = "postgresql://localhost/myapp_dev"
Add to .gitignore:
# Mise local configuration (contains secrets)
.mise.local.toml
mise.local.toml
4. Configure Environment-Specific Settings #
For different environments, use profile-specific files:
1# .mise.development.toml
2[env]
3LOG_LEVEL = "debug"
4API_URL = "http://localhost:3000"
5
6# .mise.production.toml
7[env]
8LOG_LEVEL = "info"
9API_URL = "https://api.production.com"
Activate with:
1MISE_ENV=production mise run deploy
Environment Variable Best Practices #
Loading External Files #
1[env]
2# Load variables from .env file
3_.file = ".env"
4
5# Or load multiple files (later files override earlier)
6_.file = [".env", ".env.local"]
Required Variables #
Enforce that certain variables must be set:
1[env]
2# This MUST be defined somewhere (env, .env, or mise.local.toml)
3DATABASE_URL = { required = true }
4
5# With helpful error message
6API_KEY = { required = true, help = "Get your API key from https://dashboard.example.com" }
Sensitive Value Handling #
Mark sensitive values to prevent them from appearing in logs:
1[env]
2# Single variable redaction
3API_KEY = { value = "secret", redact = true }
4
5# Redact all variables matching patterns
6_.redactions = ["*_KEY", "*_SECRET", "*_TOKEN"]
Template Variables #
Use dynamic values with templates:
1[env]
2# Project root directory
3PROJECT_ROOT = "{{ config_root }}"
4
5# Home directory
6CONFIG_DIR = "{{ home }}/.config/myapp"
7
8# Current timestamp (useful for build info)
9BUILD_TIME = "{{ now() }}"
10
11# Tool-dependent paths (resolved after tools are installed)
12MY_VAR = { value = "bun path: {{ which('bun') }}", tools = true }
Task Runner Best Practices #
Defining Tasks #
1# Simple tasks
2[tasks]
3build = "bun run build"
4test = "bun test"
5
6# Detailed task configuration
7[tasks.dev]
8description = "Start development with file watching"
9run = "bun run --watch src/index.ts"
10dir = "{{config_root}}" # Run from project root
11
12[tasks.deploy]
13description = "Build and deploy to production"
14run = """
15bun run build
16rsync -av dist/ server:/var/www/
17"""
18depends = ["lint", "test"] # Run these first
19env = { NODE_ENV = "production" } # Task-specific env vars
Parallel Task Execution #
Run multiple independent tasks in parallel:
1# Run lint, test, and typecheck in parallel
2mise run lint ::: test ::: typecheck
File Watching #
1[tasks.watch]
2description = "Watch for changes and rebuild"
3run = "bun run build"
4sources = ["src/**/*.ts"] # Watch these files
5outputs = ["dist/**"] # Skip if outputs newer than sources
6
7[tasks.dev-watch]
8description = "Development with auto-restart"
9run = "mise watch -r run dev"
Using Executable Task Files #
For complex tasks, create executable files in mise-tasks/:
1# mise-tasks/setup
2#!/usr/bin/env bash
3set -euo pipefail
4
5echo "Installing dependencies..."
6bun install
7
8echo "Setting up database..."
9bun run db:migrate
10
11echo "Done!"
Make it executable and run:
1chmod +x mise-tasks/setup
2mise run setup
Recommended Project Structure #
your-project/
├── .mise.toml # Committed - tools, env vars, tasks
├── .mise.local.toml # NOT committed - secrets, personal prefs
├── .mise.development.toml # Optional - dev-specific settings
├── .mise.production.toml # Optional - prod-specific settings
├── mise-tasks/ # Optional - complex executable tasks
│ ├── setup
│ └── deploy
├── src/
│ └── index.ts
├── tests/
├── biome.json
├── tsconfig.json
├── package.json
└── .gitignore
Complete Example: Full Project Configuration #
1# .mise.toml
2min_version = "2024.11.0"
3
4[tools]
5bun = "1.1"
6
7[env]
8# Standard development settings
9NODE_ENV = "development"
10LOG_LEVEL = "debug"
11
12# Project paths
13PROJECT_ROOT = "{{ config_root }}"
14_.path = ["./node_modules/.bin"]
15
16# Redact sensitive patterns
17_.redactions = ["*_KEY", "*_SECRET", "*_TOKEN", "*_PASSWORD"]
18
19# Mark critical vars as required (must be in .mise.local.toml or environment)
20# DATABASE_URL = { required = true }
21
22[tasks.dev]
23description = "Start development server"
24run = "bun run --watch src/index.ts"
25
26[tasks.test]
27description = "Run test suite"
28run = "bun test"
29
30[tasks.test-watch]
31description = "Run tests in watch mode"
32run = "bun test --watch"
33
34[tasks.lint]
35description = "Lint and format code"
36run = "bunx --bun biome check --write ./src"
37
38[tasks.lint-check]
39description = "Check lint without fixing (for CI)"
40run = "bunx --bun biome ci ./src"
41
42[tasks.typecheck]
43description = "TypeScript type checking"
44run = "bunx --bun tsc --noEmit"
45
46[tasks.check]
47description = "Run all quality checks"
48depends = ["lint-check", "typecheck", "test"]
49
50[tasks.build]
51description = "Build for production"
52run = "bun build ./src/index.ts --outdir ./dist --target bun"
53env = { NODE_ENV = "production" }
54
55[tasks.clean]
56description = "Remove build artifacts"
57run = "rm -rf dist node_modules"
Integration with CI/CD #
Mise can generate CI configuration:
1mise generate github-action
Or manually in .github/workflows/ci.yml:
1name: CI
2on: [push, pull_request]
3
4jobs:
5 check:
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v4
9
10 - name: Install mise
11 uses: jdx/mise-action@v2
12
13 - name: Install dependencies
14 run: bun install
15
16 - name: Run checks
17 run: mise run check
Migration from Other Tools #
From asdf #
1# Convert .tool-versions to .mise.toml
2mise config generate --tool-versions
From nvm #
Mise automatically reads .nvmrc files, but you can convert:
1# If you have .nvmrc with "22"
2mise use node@22
3# This creates/updates .mise.toml
From direnv #
Move environment variables from .envrc to .mise.toml:
1# Old .envrc
2export NODE_ENV=development
3export API_URL=http://localhost:3000
4
5# New .mise.toml [env] section
6[env]
7NODE_ENV = "development"
8API_URL = "http://localhost:3000"
Recommendation #
Use mise as the single source of truth for this project. The configuration should include:
- Tool versions - Pin Bun version for team consistency
- Environment variables - Replace .env files with mise's typed configuration
- Tasks - Define dev, test, lint, build tasks to replace npm scripts
- Secrets handling - Use
.mise.local.toml(gitignored) for API keys
This approach:
- Reduces tool count (no separate direnv, nvm, Make)
- Ensures team members use identical tool versions
- Makes onboarding trivial (
mise installsets everything up) - Provides faster shell startup than alternatives
When NOT to Use This #
-
Node.js-only projects with simple needs: If you only need Node.js and have no environment variable requirements,
nvmwith.nvmrcis simpler and more widely understood. -
Docker-first development: If everyone develops inside Docker containers, mise's tool management is redundant—the Dockerfile already specifies exact versions.
-
Strict corporate tool restrictions: Some enterprises restrict which development tools can be installed. Check with your IT/security team before adopting mise.
-
Legacy projects with complex Makefiles: If you have extensive Make-based builds with many targets and dependencies, migrating to mise tasks may not be worth the effort. Mise tasks are simpler but less powerful than Make.
-
Projects requiring remote task caching: If you need distributed build caching (like Turborepo or Nx), mise's task runner is too simple. Mise explicitly will not add remote caching features.
Sources #
- Mise Official Documentation
- Getting Started with Mise | Better Stack
- Mise Walkthrough Guide
- Mise Environments Documentation
- Mise Configuration Reference
- Mise Tasks Documentation
- Mise vs asdf Comparison | Better Stack
- mise-en-place welcomes 2025 | GitHub Discussion
- Managing Development Environments with Mise | James Carr