Skip to content

Automating Repetitive Tasks as a Developer

Practical strategies for spotting and automating repetitive development work — shell scripts, code generators, Git aliases and scaffolding tools.

5 min read
Flowchart showing manual repetitive tasks being replaced by automated scripts and tools

Most developers spend hours each week on tasks they have done a hundred times before — creating boilerplate files, setting up test fixtures, deploying to staging, renaming batches of files. These tasks are not hard, but they are interruptive. Automating them eliminates an entire class of context switches and frees your brain for actual problem-solving.

The rule of thumb: if you do something more than three times, automate it. If the automation takes less time than doing the task five more times manually, it pays for itself quickly. The secondary benefit is consistency — automated tasks do not forget steps or introduce typos.

Identifying What to Automate

Start by keeping a log of repetitive tasks for one week. Most developers are surprised by how much time goes to mechanical work.

markdownmarkdown
## Automation Candidates (1 week log)
 
### High Frequency (daily)
- Creating new React component files (same structure every time)
- Running database migrations before starting dev server
- Switching between Git branches and rebuilding
 
### Medium Frequency (weekly)
- Setting up test fixtures for integration tests
- Generating API client code from OpenAPI specs
- Creating pull request descriptions from commit history
 
### Low Frequency (monthly)
- Spinning up a new microservice from template
- Rotating API keys across environments
- Generating release notes from merged PRs
tstypescript
// ❌ Manual component creation — every single time
// 1. Create src/components/UserProfile/
// 2. Create UserProfile.tsx with boilerplate
// 3. Create UserProfile.test.tsx with test structure
// 4. Create index.ts with export
// 5. Add to component barrel file
// Time: 3-5 minutes, repeated 3x per day = 15 minutes/day
 
// ✅ Automated — one command
// $ gen component UserProfile
// Creates all 4 files with correct structure
// Time: 5 seconds

Shell Aliases and Functions

The lowest-effort automation is shell aliases and functions. They wrap multi-step commands into single invocations.

shbash
# ~/.zshrc or ~/.bashrc
 
# Git shortcuts
alias gs='git status'
alias gco='git checkout'
alias gp='git pull --rebase'
alias gcm='git commit -m'
 
# Development workflow
alias dev='npm run dev'
alias build='npm run build'
alias test='npm run test'
alias lint='npm run lint -- --fix'
 
# Complex workflows as functions
function fresh() {
  # Fresh start: pull latest, install deps, run migrations, start dev
  echo "🔄 Pulling latest changes..."
  git pull --rebase origin main
  echo "📦 Installing dependencies..."
  npm ci
  echo "🗄️  Running migrations..."
  npm run db:migrate
  echo "🚀 Starting dev server..."
  npm run dev
}
 
function branch() {
  # Create a new feature branch from latest main
  local branch_name=$1
  if [ -z "$branch_name" ]; then
    echo "Usage: branch feat/my-feature"
    return 1
  fi
  git checkout main
  git pull --rebase
  git checkout -b "$branch_name"
  echo "Created and switched to $branch_name"
}
shbash
# ❌ Multiple commands every time you start work
git checkout main
git pull --rebase
npm ci
npm run db:migrate
npm run dev
# Time: 2 minutes of typing + waiting
 
# ✅ One command
fresh
# Same result, zero thinking, zero typos

Code Generators

For repetitive file creation, a code generator eliminates boilerplate and enforces project conventions.

tstypescript
// scripts/generate-component.ts
import fs from 'fs';
import path from 'path';
 
interface ComponentConfig {
  name: string;
  directory: string;
  withTests: boolean;
  withStories: boolean;
}
 
function generateComponent(config: ComponentConfig): void {
  const dir = path.join(config.directory, config.name);
  fs.mkdirSync(dir, { recursive: true });
 
  // Component file
  const component = `interface ${config.name}Props {
  className?: string;
}
 
export function ${config.name}({ className }: ${config.name}Props) {
  return (
    <div className={className}>
      <h2>${config.name}</h2>
    </div>
  );
}
`;
  fs.writeFileSync(path.join(dir, `${config.name}.tsx`), component);
 
  // Test file
  if (config.withTests) {
    const test = `import { render, screen } from '@testing-library/react';
import { ${config.name} } from './${config.name}';
 
describe('${config.name}', () => {
  it('renders without crashing', () => {
    render(<${config.name} />);
    expect(screen.getByText('${config.name}')).toBeInTheDocument();
  });
});
`;
    fs.writeFileSync(
      path.join(dir, `${config.name}.test.tsx`),
      test
    );
  }
 
  // Index file
  const index = `export { ${config.name} } from './${config.name}';\n`;
  fs.writeFileSync(path.join(dir, 'index.ts'), index);
 
  console.log(`Created ${config.name} in ${dir}`);
}
 
// CLI entry point
const name = process.argv[2];
if (!name) {
  console.error('Usage: tsx scripts/generate-component.ts MyComponent');
  process.exit(1);
}
 
generateComponent({
  name,
  directory: 'src/components',
  withTests: true,
  withStories: false,
});

Git Hooks for Automated Checks

Git hooks automate quality checks at the right moments — before committing, before pushing. They prevent common mistakes from reaching the repository.

shbash
# .husky/pre-commit — lint only changed files
npx lint-staged
 
# .husky/commit-msg — enforce commit message format
#!/bin/sh
COMMIT_MSG=$(cat "$1")
PATTERN="^(feat|fix|docs|style|refactor|perf|test|chore|ci|revert)(\(.+\))?: .{1,100}$"
 
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
  echo "❌ Commit message does not match conventional format."
  echo "   Expected: type(scope): description"
  echo "   Example: feat(auth): add password reset flow"
  exit 1
fi
jsonjson
// lint-staged.config.json — auto-format on commit
{
  "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
  "*.{css,scss}": ["prettier --write"],
  "*.md": ["prettier --write"]
}

Database and API Automation

Automate repetitive database operations and API client generation to eliminate manual sync work.

shbash
#!/bin/bash
# scripts/sync-staging.sh — pull staging database for local development
 
set -euo pipefail
 
DB_NAME="myapp_dev"
STAGING_URL="${STAGING_DATABASE_URL:?Set STAGING_DATABASE_URL}"
 
echo "⬇️  Dumping staging database..."
pg_dump "$STAGING_URL" \
  --no-owner \
  --no-privileges \
  --exclude-table='sessions' \
  --exclude-table='audit_logs' \
  > /tmp/staging_dump.sql
 
echo "🗑️  Dropping local database..."
dropdb --if-exists "$DB_NAME"
createdb "$DB_NAME"
 
echo "📥 Restoring to local..."
psql "$DB_NAME" < /tmp/staging_dump.sql
 
echo "🔒 Anonymizing PII..."
psql "$DB_NAME" <<SQL
  UPDATE users SET
    email = 'user_' || id || '@test.local',
    first_name = 'Test',
    last_name = 'User_' || id;
SQL
 
rm /tmp/staging_dump.sql
echo "✅ Local database synced with anonymized staging data"
tstypescript
// scripts/generate-api-client.ts
// Auto-generate TypeScript API client from OpenAPI spec
 
import { execSync } from 'child_process';
 
const SPEC_URL = 'https://api.example.com/openapi.json';
const OUTPUT_DIR = 'src/api/generated';
 
// Generate client using openapi-typescript-codegen
execSync(
  `npx openapi-typescript-codegen \
    --input ${SPEC_URL} \
    --output ${OUTPUT_DIR} \
    --client fetch \
    --useOptions`,
  { stdio: 'inherit' }
);
 
console.log(`API client generated in ${OUTPUT_DIR}`);

Project Scaffolding

For teams that create new services or packages regularly, a scaffolding tool ensures every project starts with the same structure, configurations, and CI pipelines.

tstypescript
// scripts/scaffold-service.ts
import fs from 'fs';
import path from 'path';
 
interface ServiceConfig {
  name: string;
  port: number;
  hasDatabase: boolean;
  hasRedis: boolean;
}
 
function scaffoldService(config: ServiceConfig): void {
  const base = path.join('services', config.name);
 
  const structure: Record<string, string> = {
    'src/index.ts': generateEntryPoint(config),
    'src/routes/health.ts': generateHealthRoute(config),
    'Dockerfile': generateDockerfile(config),
    'docker-compose.yml': generateDockerCompose(config),
    '.env.example': generateEnvExample(config),
    'tsconfig.json': JSON.stringify(tsconfig, null, 2),
    'package.json': JSON.stringify(
      generatePackageJson(config),
      null,
      2
    ),
  };
 
  for (const [filePath, content] of Object.entries(structure)) {
    const fullPath = path.join(base, filePath);
    fs.mkdirSync(path.dirname(fullPath), { recursive: true });
    fs.writeFileSync(fullPath, content);
  }
 
  console.log(`Scaffolded service '${config.name}' in ${base}/`);
  console.log(`Next steps:`);
  console.log(`  cd ${base}`);
  console.log(`  npm install`);
  console.log(`  npm run dev`);
}
shbash
# ❌ Manual service creation
# 1. Create directory structure (5 min)
# 2. Copy and modify Dockerfile from another service (10 min)
# 3. Set up tsconfig, package.json, eslint config (15 min)
# 4. Create health check route (5 min)
# 5. Set up docker-compose with database (10 min)
# 6. Configure CI pipeline (15 min)
# Time: ~1 hour, inconsistent across services
 
# ✅ Scaffolded in seconds
npx tsx scripts/scaffold-service.ts --name user-service --port 3001 --db --redis
# Time: 10 seconds, consistent every time

Key Takeaways

  1. Log repetitive tasks for one week — you cannot automate what you have not identified; the frequency and time cost will surprise you
  2. Start with shell aliases — the fastest way to eliminate multi-step commands; five minutes of setup saves months of typing
  3. Build code generators for boilerplate — any file structure created more than three times should have a generator
  4. Automate database syncs with anonymization — staging data in local development accelerates debugging without exposing PII
  5. Use scaffolding for new projects — every new service or package should start from a consistent template, not a copy-paste from another project
  6. Invest in automation proportionally — spend 30 minutes automating something you do daily, not something you do once a year
Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX