Best Practices for Using Mise to Maintain Project Structure and Manage Environment Variables

· combray's blog


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:

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:

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
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:

  1. Tool versions - Pin Bun version for team consistency
  2. Environment variables - Replace .env files with mise's typed configuration
  3. Tasks - Define dev, test, lint, build tasks to replace npm scripts
  4. Secrets handling - Use .mise.local.toml (gitignored) for API keys

This approach:

When NOT to Use This #

Sources #

last updated: