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.
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- Stage 1$layout
Positional values or no named parameters - Stage 2Format-Table
Positional 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, PurposeWhat 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
Try it
Illustrative output · values vary by environment
Example reader
Inspect this page's example
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.
The sandbox is visible now. Sign in with Google only when you run or check a task; commands and temporary files are discarded when you leave.
Run a command to inspect its result.