Modules

Organize Lua code into reusable modules. Essential for structured Neovim configurations.

require

Basic Usage

-- Load module
local utils = require("utils")
local json = require("json")

-- Module returns a table, function, or value
local result = require("mymodule")

-- Modules are cached
local a = require("utils")  -- Loads and caches
local b = require("utils")  -- Returns cached version
print(a == b)  -- true

Module Search Path

-- Lua searches package.path for modules
print(package.path)
-- ./?.lua;./?/init.lua;/usr/share/lua/5.4/?.lua;...

-- ? is replaced with module name
-- require("mymodule") searches:
-- ./mymodule.lua
-- ./mymodule/init.lua
-- /usr/share/lua/5.4/mymodule.lua
-- ...

-- Add custom path
package.path = package.path .. ";/my/custom/?.lua"

-- C modules use package.cpath
print(package.cpath)

Neovim Module Path

-- Neovim adds these to package.path:
-- ~/.config/nvim/lua/?.lua
-- ~/.config/nvim/lua/?/init.lua

-- So require("mymodule") finds:
-- ~/.config/nvim/lua/mymodule.lua
-- ~/.config/nvim/lua/mymodule/init.lua

-- Nested modules use dots
require("plugins.telescope")  -- lua/plugins/telescope.lua
require("config.keymaps")     -- lua/config/keymaps.lua

Creating Modules

Basic Module Pattern

-- lua/utils.lua
local M = {}

function M.validate_ip(ip)
    local pattern = "^%d+%.%d+%.%d+%.%d+$"
    return string.match(ip, pattern) ~= nil
end

function M.validate_mac(mac)
    local pattern = "^%x%x:%x%x:%x%x:%x%x:%x%x:%x%x$"
    return string.match(mac, pattern) ~= nil
end

-- Private function (local, not in M)
local function internal_helper()
    return "internal"
end

return M

-- Usage:
-- local utils = require("utils")
-- utils.validate_ip("10.50.1.20")

Module with State

-- lua/logger.lua
local M = {}

-- Module state (private)
local level = "INFO"
local log_file = nil

-- Public API
function M.set_level(new_level)
    level = new_level
end

function M.info(msg)
    if level ~= "DEBUG" then
        print("[INFO] " .. msg)
    end
end

function M.debug(msg)
    if level == "DEBUG" then
        print("[DEBUG] " .. msg)
    end
end

function M.error(msg)
    print("[ERROR] " .. msg)
end

return M

Module with Setup

-- lua/myplugin.lua (Neovim plugin pattern)
local M = {}

-- Default configuration
M.config = {
    enabled = true,
    key = "<leader>x",
    style = "minimal"
}

function M.setup(opts)
    -- Merge user options with defaults
    M.config = vim.tbl_deep_extend("force", M.config, opts or {})

    if not M.config.enabled then
        return
    end

    -- Setup keymaps, autocommands, etc.
    vim.keymap.set("n", M.config.key, M.action, {
        desc = "My plugin action"
    })
end

function M.action()
    print("Plugin action with style: " .. M.config.style)
end

return M

-- Usage in init.lua:
-- require("myplugin").setup({
--     key = "<leader>m",
--     style = "fancy"
-- })

Lazy Loading

-- Only load module when first used
local M = {}

local _utils = nil
local function get_utils()
    if not _utils then
        _utils = require("utils")
    end
    return _utils
end

function M.do_something()
    local utils = get_utils()
    return utils.validate_ip("10.50.1.20")
end

return M

-- Or use Neovim's lazy loading
local utils = setmetatable({}, {
    __index = function(_, key)
        return require("utils")[key]
    end
})

Neovim Module Organization

Typical Structure

~/.config/nvim/
├── init.lua                 # Entry point
├── lua/
│   ├── config/
│   │   ├── options.lua      # vim.opt settings
│   │   ├── keymaps.lua      # Key mappings
│   │   └── autocmds.lua     # Autocommands
│   ├── plugins/
│   │   ├── init.lua         # Plugin manager setup
│   │   ├── telescope.lua    # Telescope config
│   │   └── lsp.lua          # LSP config
│   └── utils/
│       ├── init.lua         # Utility functions
│       └── keymaps.lua      # Keymap helpers

init.lua Entry Point

-- ~/.config/nvim/init.lua

-- Load configuration modules
require("config.options")
require("config.keymaps")
require("config.autocmds")

-- Load plugins
require("plugins")

Config Module Example

-- lua/config/options.lua
local opt = vim.opt

-- Line numbers
opt.number = true
opt.relativenumber = true

-- Tabs/indentation
opt.tabstop = 4
opt.shiftwidth = 4
opt.expandtab = true

-- Search
opt.ignorecase = true
opt.smartcase = true

-- UI
opt.termguicolors = true
opt.signcolumn = "yes"

Keymap Module

-- lua/config/keymaps.lua
local map = vim.keymap.set

-- Leader
vim.g.mapleader = " "
vim.g.maplocalleader = " "

-- Better navigation
map("n", "<C-h>", "<C-w>h", { desc = "Move to left window" })
map("n", "<C-j>", "<C-w>j", { desc = "Move to lower window" })
map("n", "<C-k>", "<C-w>k", { desc = "Move to upper window" })
map("n", "<C-l>", "<C-w>l", { desc = "Move to right window" })

-- Clear search
map("n", "<Esc>", "<cmd>nohlsearch<CR>", { desc = "Clear search" })

-- Save
map("n", "<leader>w", "<cmd>w<CR>", { desc = "Save file" })

Plugin Spec Module

-- lua/plugins/telescope.lua (for lazy.nvim)
return {
    "nvim-telescope/telescope.nvim",
    dependencies = { "nvim-lua/plenary.nvim" },
    cmd = "Telescope",
    keys = {
        { "<leader>ff", "<cmd>Telescope find_files<cr>", desc = "Find Files" },
        { "<leader>fg", "<cmd>Telescope live_grep<cr>", desc = "Live Grep" },
        { "<leader>fb", "<cmd>Telescope buffers<cr>", desc = "Buffers" },
    },
    opts = {
        defaults = {
            prompt_prefix = " ",
            selection_caret = " ",
        },
    },
}

Module Reloading

-- Force reload module (for development)
package.loaded["mymodule"] = nil
local mymodule = require("mymodule")

-- Reload all modules matching pattern
local function reload_module(pattern)
    for name in pairs(package.loaded) do
        if name:match(pattern) then
            package.loaded[name] = nil
        end
    end
end

reload_module("^config")  -- Reload all config.* modules

-- In Neovim, use plenary for hot reload
-- :lua require("plenary.reload").reload_module("mymodule")

Package Patterns

Namespace Module

-- lua/myns/init.lua
local M = {}

-- Re-export submodules
M.utils = require("myns.utils")
M.config = require("myns.config")
M.api = require("myns.api")

return M

-- Usage:
-- local myns = require("myns")
-- myns.utils.helper()
-- myns.api.call()

Singleton

-- lua/database.lua
local M = {}

local instance = nil

function M.get_instance()
    if not instance then
        instance = {
            connected = false,
            data = {}
        }
    end
    return instance
end

function M.connect()
    local db = M.get_instance()
    db.connected = true
end

return M

-- All requires get same instance
local db1 = require("database")
local db2 = require("database")
db1.connect()
print(db2.get_instance().connected)  -- true

Next Module

Coroutines - Cooperative multitasking and async patterns.