PowerShell Genie

PowerShell Module architecture

A module that other people use needs structure. The layout that has settled as conventional separates public from private and keeps one function per file.

Lesson 3 min read Windows PowerShell 5.1 and PowerShell 7

Documentation: Microsoft Learn

Overview

The manifest, created with New-ModuleManifest, is the contract. Set ModuleVersion, RootModule, and above all FunctionsToExport as an explicit list rather than a wildcard. A wildcard export means every function is public, including helpers you meant to keep internal, and it also disables the command discovery cache, which makes auto-loading slower for everyone. Listing exports explicitly is the single highest-value thing in a manifest.

RequiredModules declares dependencies so PowerShell loads them, and CompatiblePSEditions declares whether you support Desktop, Core or both. Both save your users from finding out the hard way.

The root .psm1 dot-sources the files in Public and Private, then exports only the public ones. One function per file keeps diffs readable and makes tests easy to locate.

Version with SemVer and mean it. A breaking change to a parameter is a major version, and shipping it as a patch breaks scripts you cannot see.

Include comment-based help on every public function. Get-Help on your module should work the same way it does on a built-in one.

Inspect this example’s structure

Read the command and its supplied arguments. This example requires real PowerShell or its module; this diagram does not execute it.

$layout = @( [pscustomobject]@{ Path = 'MyModule\MyModule.psd1';Purpose = 'manifest: version, exports, dependencies' } [pscustomobject]@{ Path = 'MyModule\MyModule.psm1';Purpose = 'root module: dot-sources Public and Private' } [pscustomobject]@{ Path = 'MyModule\Public\Get-Thing.ps1';Purpose = 'one exported function per file' } [pscustomobject]@{ Path = 'MyModule\Private\Helper.ps1';Purpose = 'internal, not exported' } [pscustomobject]@{ Path = 'MyModule\Tests\Get-Thing.Tests.ps1';Purpose = 'Pester tests' }) $layout | Format-Table Path, Purpose
  1. Stage 1$layoutPositional values or no named parameters
  2. Stage 2Format-TablePositional values or no named parameters

    An example to inspect

    Preview the example. Sign in for the full lesson and activities.

    $layout = @(
    [pscustomobject]@{ Path = 'MyModule\MyModule.psd1';Purpose = 'manifest: version, exports, dependencies' }
    [pscustomobject]@{ Path = 'MyModule\MyModule.psm1';Purpose = 'root module: dot-sources Public and Private' }
    [pscustomobject]@{ Path = 'MyModule\Public\Get-Thing.ps1';Purpose = 'one exported function per file' }
    [pscustomobject]@{ Path = 'MyModule\Private\Helper.ps1';Purpose = 'internal, not exported' }
    [pscustomobject]@{ Path = 'MyModule\Tests\Get-Thing.Tests.ps1';Purpose = 'Pester tests' })
    $layout | Format-Table Path, Purpose

    What happens

    This prints the layout as a table. Public and Private as sibling directories is what makes the export boundary obvious to a reader, and Tests alongside them keeps tests next to the code rather than in a separate repository nobody runs.

    Worth remembering

    • List FunctionsToExport explicitly; a wildcard exports helpers and slows discovery.
    • Declare RequiredModules and CompatiblePSEditions in the manifest.
    • One function per file, split into Public and Private.
    • Use SemVer honestly: a parameter change is a major version.

    Primary documentation

    PowerShell objects and pipelines documentation

    Try it

    Illustrative output · values vary by environment

    1PS> $layout = @(
    2PS> [pscustomobject]@{ Path = 'MyModule\MyModule.psd1';Purpose = 'manifest: version, exports, dependencies' }
    3PS> [pscustomobject]@{ Path = 'MyModule\MyModule.psm1';Purpose = 'root module: dot-sources Public and Private' }
    4PS> [pscustomobject]@{ Path = 'MyModule\Public\Get-Thing.ps1';Purpose = 'one exported function per file' }
    5PS> [pscustomobject]@{ Path = 'MyModule\Private\Helper.ps1';Purpose = 'internal, not exported' }
    6PS> [pscustomobject]@{ Path = 'MyModule\Tests\Get-Thing.Tests.ps1';Purpose = 'Pester tests' })
    7PS> $layout | Format-Table Path, Purpose

    Example reader

    Inspect this page's example

    Fictional browser data when supported

    Run supported commands against the practice data, or inspect the example without executing it.

    Ready to inspect.

      Prints the conventional module layout and what each part is for.

      Show reference output

      Illustrative output · values vary by environment

      Path                                  Purpose
      ----                                  -------
      MyModule\MyModule.psd1                 manifest: version, exports, dependencies
      MyModule\MyModule.psm1                 root module: dot-sources Public and Private
      MyModule\Public\Get-Thing.ps1          one exported function per file
      MyModule\Private\Helper.ps1           internal, not exported
      MyModule\Tests\Get-Thing.Tests.ps1    Pester tests

      Practise this

      Export a module function

      Define Get-ServiceSummary to run the requested pipeline: Get-Service | Select-Object Name,Status. Call it and return its output. Export it with Export-ModuleMember before calling it.

      Review the lesson

      Keep going

      Next: PowerShell Desired State Configuration
      Genie guidePowerShell Module architecture

      A module that other people use needs structure. The layout that has settled as conventional separates public from private and keeps one function per file.