Building & Publishing Your Own Modules

PowerShell Intermediate/Advanced

Chapter 8 · Building & Publishing Your Own Modules

Fundamentals 11 covered consuming modules — Get-Module, Import-Module, installing one from the Gallery. This chapter is about being on the other side of that: turning a folder of your own functions into something someone else could genuinely Install-Module, complete with the one file most people skip when they're just experimenting, and the reason skipping it eventually causes real problems.

Two Files: .psm1 (Code) and .psd1 (Manifest)

A script module is just a .ps1 file renamed to .psm1, containing function definitions — nothing more is technically required to import and use one. A real, publishable module adds a manifest, a .psd1 file: pure metadata (version, author, which functions are actually meant to be public, required PowerShell version, dependencies) that PowerShell can read without ever running any of the module's own code.

Export-ModuleMember: Controlling What's Actually Public

# MyTools.psm1 function Get-Greeting { param([string]$Name) "Hello, $Name!" } function Get-InternalHelper { # an internal helper — not meant to be called by whoever imports this module "internal only" } Export-ModuleMember -Function Get-Greeting
Without Export-ModuleMember, everything gets exported by default
Skip Export-ModuleMember entirely, and every function defined in the .psm1 — including Get-InternalHelper, which was never meant to be part of any public API — becomes fully visible and callable by anyone who imports the module, discoverable through the exact same Get-Command Fundamentals 3 already covered. Explicitly listing only the functions actually meant to be public is what keeps a module's real interface honest.

Module Folder Structure & PSModulePath

$env:PSModulePath -split ';' # the semicolon-separated list of folders PowerShell searches for modules # The folder name must match the module's own base name exactly: # MyTools\ # ├── MyTools.psm1 # └── MyTools.psd1

A module named MyTools that doesn't sit inside a MyTools\ folder, on one of the paths $env:PSModulePath lists, simply won't be found by Import-Module or the auto-loading Fundamentals 11 already relied on.

New-ModuleManifest: Generating a Real Manifest

New-ModuleManifest -Path .\MyTools\MyTools.psd1 ` -RootModule "MyTools.psm1" ` -ModuleVersion "1.0.0" ` -Author "Philip Osztromok" ` -FunctionsToExport @("Get-Greeting")

New-ModuleManifest generates a correctly structured .psd1 with all the right fields — far more reliable than hand-writing one from scratch and risking a typo in a key PowerShell's own tooling actually checks.

Publishing to the PowerShell Gallery

Publish-Module -Path .\MyTools -NuGetApiKey $apiKey

Publishing requires an account and a NuGet API key from powershellgallery.com — the same PSGallery Fundamentals 11 already covered from the consuming side, via Find-Module/Install-Module.

Each published version is immutable — republishing the same version fails
The Gallery treats every published version as permanent and unchangeable — running Publish-Module again with the identical ModuleVersion still set in the manifest fails outright, even for a genuinely tiny fix. The version has to be bumped first:
Update-ModuleManifest -Path .\MyTools\MyTools.psd1 -ModuleVersion "1.0.1" Publish-Module -Path .\MyTools -NuGetApiKey $apiKey
Semantic versioning (Major.Minor.Patch) is the expected convention — a patch bump like 1.0.01.0.1 for a small fix, a minor bump for a genuinely new feature, a major bump for a breaking change.

FunctionsToExport vs. Export-ModuleMember: Why Explicit Beats Wildcard

Both the manifest's own FunctionsToExport and the .psm1's Export-ModuleMember can restrict what's public — but leaving FunctionsToExport as the default wildcard ('*') carries a real, system-wide performance cost, not just a style preference:

The central fact this chapter is built on
A manifest with an explicit FunctionsToExport list is pure, static metadata — PowerShell can read exactly what a module exports directly from the .psd1 file, without ever loading or running a single line of the module's own code. FunctionsToExport = '*' throws that advantage away: PowerShell has no choice but to actually load the module and introspect it to discover what it exports, every time module auto-discovery needs an answer. Multiply that across every module on a system using a wildcard, and Fundamentals 11's own "most modules just auto-load, no explicit Import-Module needed" convenience gets measurably slower for everyone — auto-loading only stays fast because well-behaved modules declare their exports explicitly, rather than making the shell do the work of finding out.
A first practical habit
Let New-ModuleManifest -FunctionsToExport match exactly the same list already passed to Export-ModuleMember in the .psm1 — keeping both declarations in sync avoids a confusing mismatch between what the manifest promises and what the code actually exposes.

Hands-On Exercises

Exercise 1

Explain what happens to Get-InternalHelper if Export-ModuleMember -Function Get-Greeting is never added to MyTools.psm1 at all, referencing this chapter's own default-export behavior.

📄 View solution
Exercise 2

Explain why running Publish-Module a second time with the manifest's ModuleVersion left unchanged fails, and what you'd need to do first before it can succeed.

📄 View solution
Exercise 3

Explain why FunctionsToExport @('Get-Greeting') in a manifest is genuinely better for system-wide performance than leaving it as FunctionsToExport = '*', referencing this chapter's own explanation of module auto-discovery.

📄 View solution

Chapter 8 Quick Reference

  • .psm1 — the module's actual code; .psd1 — its manifest, pure metadata read without running any code
  • Export-ModuleMember -Function — restricts what's public; without it, every function in the .psm1 is exported by default
  • Folder name must match the module's base name, and sit on a path listed in $env:PSModulePath
  • New-ModuleManifest — generates a correctly structured .psd1 rather than hand-writing one
  • Publish-Module -NuGetApiKey — publishes to the PowerShell Gallery; requires an account and API key
  • Published versions are immutable — republishing the same ModuleVersion fails; bump it with Update-ModuleManifest first
  • Explicit FunctionsToExport beats a wildcard — lets PowerShell read exports as static metadata instead of loading the whole module just to find out, keeping system-wide auto-discovery fast
  • Next chapter: Introduction to Desired State Configuration (DSC)