Modules and the require System

· 5 min read · Updated March 17, 2026 · beginner
modules require beginner lua-fundamentals

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"):

  1. Lua checks if the module is already loaded in package.loaded
  2. If not, it searches for the module file using package.path
  3. Once found, Lua executes the file and stores the result in package.loaded
  4. 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.path directories
  • The filename matches exactly (case-sensitive on Linux)
  • The file has .lua extension or you’re using init.lua pattern

Summary

Modules are essential for building maintainable Lua applications. The key points to remember:

  • Modules are .lua files that return a table of functions
  • Use require to load modules — it caches results in package.loaded
  • Set up package.path to tell Lua where to find your modules
  • Prefer the modern table-return pattern over the legacy module function
  • Return a single table, keep functions local, and organize with init.lua for packages

With these fundamentals, you can structure any Lua project cleanly and scale it as it grows.