Modules and the require System
As your Lua programs grow, you’ll need a way to organize code into reusable pieces. That’s where modules come in. Modules let you split your code across files, share functionality between projects, and keep your codebase maintainable. In this tutorial, you’ll learn how modules work in Lua, how to use require to load them, and best practices for organizing your code.
What Are Modules?
A module in Lua is simply a file containing Lua code that you can load into your program. Modules help you:
- Encapsulate code — Keep related functions and data together
- Avoid global namespace pollution — Variables stay contained within modules
- Reuse code — Import functionality across different projects
- Maintainability — Smaller, focused files are easier to understand and debug
Every .lua file can serve as a module. When you load it, Lua executes the file and returns whatever value you specify as the module.
Creating Your First Module
Let’s create a simple module that provides mathematical utilities. Create a file called mathutil.lua:
-- mathutil.lua
local M = {}
function M.add(a, b)
return a + b
end
function M.subtract(a, b)
return a - b
end
function M.square(x)
return x * x
end
return M
This module returns a table containing all its functions. The table acts as a namespace, keeping everything organized. Now let’s use this module in another file:
-- main.lua
local mathutil = require("mathutil")
print(mathutil.add(3, 5)) -- Output: 8
print(mathutil.subtract(10, 4)) -- Output: 6
print(mathutil.square(4)) -- Output: 16
The require function loads the module and returns whatever value the module returns. In this case, it’s a table with functions we can call using dot notation.
How require Works
The require function is Lua’s primary mechanism for loading modules. Here’s what happens when you call require("mathutil"):
- Lua checks if the module is already loaded in
package.loaded - If not, it searches for the module file using
package.path - Once found, Lua executes the file and stores the result in
package.loaded - The result is returned to your code
The key insight is that require caches loaded modules. If you call require multiple times for the same module, Lua returns the cached version without re-executing the file:
local m1 = require("mathutil")
local m2 = require("mathutil")
print(m1 == m2) -- Output: true (same table, cached)
This behavior is efficient but can surprise beginners expecting fresh loads on each call.
Understanding package.path
Lua finds modules by searching directories listed in package.path. This is a colon-separated list of paths (semicolon-separated on Windows). Each path can contain a question mark ? that Lua replaces with the module name:
-- Display the default search path
for path in string.gmatch(package.path, "[^;]+") do
print(path)
end
Typical output includes:
./?.lua
./?/init.lua
/usr/local/share/lua/5.4/?.lua
/usr/local/share/lua/5.4/?/init.lua
You can add your own directories by modifying package.path:
-- Add current directory's lib subfolder to search path
package.path = package.path .. ";./lib/?.lua;/lib/?/init.lua"
This lets you organize modules in subdirectories and still find them with require.
Using package.preload
Sometimes you want to load a module without a file — perhaps it’s generated dynamically or lives in C code. package.preload lets you define a custom loader for specific module names:
-- Register a custom loader for "virtual" modules
package.preload["virtual"] = function()
return {
name = "Virtual Module",
version = "1.0.0"
}
end
local v = require("virtual")
print(v.name) -- Output: Virtual Module
print(v.version) -- Output: 1.0.0
This is useful for embedding modules, mocking during testing, or interfacing with non-file-based resources.
module vs require
Older Lua code often uses the module function to create packages:
-- Old-style module (Lua 5.1 style)
module("mathutil", package.seeall)
function add(a, b)
return a + b
end
This approach:
- Creates a global table with the module name
- Sets up the environment to capture future function definitions
- Is considered legacy in Lua 5.2+
The modern approach uses require with a table return:
-- Modern style (recommended)
local M = {}
-- Define functions in M
return M
Prefer the modern style. It’s cleaner, works better with Lua 5.4, and avoids polluting the global namespace.
Best Practices for Organizing Modules
Follow these patterns to keep your Lua code maintainable:
Return a single table: Always return one table containing your module’s public API:
local M = {}
-- Private function (not returned)
local function internalHelper()
-- ...
end
-- Public function
function M.publicFunction()
-- ...
end
return M
Use local variables: Declare your module table as local to avoid globals:
local mymodule = {} -- Not: mymodule = {}
Name files descriptively: Match filename to module name. mathutil.lua becomes require("mathutil").
Use init.lua for packages: A file named init.lua in a directory lets you treat the directory as a package:
-- mypackage/init.lua
local M = {}
M.version = "1.0"
return M
local pkg = require("mypackage") -- Loads mypackage/init.lua
Handling Dependencies
When your modules depend on other modules, manage those dependencies clearly:
-- config.lua
local M = {}
M.defaults = {
host = "localhost",
port = 8080
}
return M
-- app.lua
local config = require("config")
local mathutil = require("mathutil")
local settings = config.defaults
settings.timeout = mathutil.square(10)
In larger projects, consider a dependency injection pattern or a module that centralizes configuration.
Common Pitfalls
Watch out for these common mistakes:
Forgetting to return: A module without a return statement returns nil:
-- broken.lua
function hello()
print("hi")
end
-- Missing return!
local b = require("broken")
print(b) -- Output: nil
Circular requires: If module A requires B and B requires A, you might get partial tables:
-- a.lua
local b = require("b")
local M = { value = "a" }
M.b = b
return M
-- b.lua
local a = require("a") -- Gets A before it's fully initialized
local M = { value = "b" }
return M
Solve this by restructuring or using lazy loading.
Wrong path: If require("mymod") fails, check that:
- The file exists in one of
package.pathdirectories - The filename matches exactly (case-sensitive on Linux)
- The file has
.luaextension or you’re usinginit.luapattern
Summary
Modules are essential for building maintainable Lua applications. The key points to remember:
- Modules are
.luafiles that return a table of functions - Use
requireto load modules — it caches results inpackage.loaded - Set up
package.pathto tell Lua where to find your modules - Prefer the modern table-return pattern over the legacy
modulefunction - Return a single table, keep functions local, and organize with
init.luafor packages
With these fundamentals, you can structure any Lua project cleanly and scale it as it grows.