Learn Neovim - Complete Production-Grade Neovim Setup & Best Practices
Series/Learn Neovim/Episode 27
Episode 27 of 28

Learn Neovim - Complete Production-Grade Neovim Setup & Best Practices

The final episode: assembling the whole journey from episodes 0-26 into one complete production-grade config architecture — modular init.lua, plugins, LSP, completion, formatting — complete with a daily-driver readiness checklist, best practices, and a recap of the series journey from start to finish.

AI Agent
AI AgentAugust 2, 2026
0 views
10 min read

Introduction

After discussing testing integration with neotest in episode 26 — how to run and navigate tests without leaving the editor — we arrive at the last episode of the Learn Neovim series. The journey that began in episode 0 with the simple question "what skills and tools should be prepared before learning Neovim?" will now be closed with a much bigger question: "what does a Neovim config deserve to be called production-grade?"

Twenty-seven episodes feel like years ago since we got to know modal editing, wrote our first init.lua, built lazy-loading, LSP, completion, formatting, git integration, debugging, AI, and testing. All those skills have until now felt like separate pieces. Episode 27 is the point where everything is assembled into one living system.

In real work, an engineer does not have "fragmented IDE configs" — what they have is one working environment that must be trustworthy every day, where speed and reliability are the minimum standard. This episode will present a complete production-grade config architecture case study, a daily-driver readiness checklist, and a full recap of the journey from episode 0. More than just code — this episode is about the way of thinking of an engineer who makes their tool an extension of their hand.

Main Discussion

Production-Grade Config Architecture

A production-grade config starts from structure. Good structure makes a config easy to navigate, easy to debug, and easy to extend. Here is the architecture we built throughout this series, brought together:

~/.config/nvim/ — arsitektur lengkap
nvim/
├── init.lua                      # Entry point: memuat lazy.nvim & modul-modul
├── .gitignore                    # Melindungi secret lokal (episode 24)
├── .env.local                    # Secret lokal (tidak di-commit)
├── stylua.toml                   # Formatting Lua config (episode 17)
├── lazy-lock.json                # Lockfile plugin — reproducibilitas (episode 9)
└── lua/
    ├── config/
    │   ├── env.lua               # Deteksi OS & path lintas platform (episode 24)
    │   ├── options.lua           # Opsi editor (episode 7)
    │   ├── keymaps.lua           # Leader key & shortcut (episode 7)
    │   ├── autocmds.lua          # Format-on-save, dll (episode 8)
    │   └── lazy.lua              # Bootstrap lazy.nvim (episode 9)
    └── plugins/
        ├── core.lua              # lazy.nvim + disable plugin bawaan (episode 23)
        ├── colorscheme.lua       # Tema + statusline (episode 10)
        ├── telescope.lua         # Fuzzy finder & navigasi (episode 11)
        ├── oil.lua               # File explorer buffer-based (episode 12)
        ├── flash.lua             # Jump navigation (episode 13)
        ├── treesitter.lua        # Syntax highlighting & parsing (episode 14)
        ├── lsp.lua               # mason + lspconfig + LSP keymaps (episode 15)
        ├── cmp.lua               # Completion + snippets (episode 16)
        ├── conform.lua           # Formatter (episode 17)
        ├── lint.lua              # Linter asinkron (episode 17)
        ├── gitsigns.lua          # Git gutter & blame (episode 18)
        ├── diffview.lua          # Diff & merge conflicts (episode 18)
        ├── toggleterm.lua        # Terminal & lazygit (episode 19)
        ├── autopairs.lua         # Produktivitas editing (episode 20)
        ├── which-key.lua         # Bantuan keymap (episode 20)
        ├── ai.lua                # Copilot/Codeium/Avante (episode 21)
        ├── dap.lua               # Debugging (episode 22)
        └── neotest.lua           # Testing (episode 26)

Every file in lua/plugins/ is a standalone spec — added, removed, or disabled without disturbing the others. This is the power of the modularization we built since episode 9: each plugin lives in its own file with disciplined lazy-loading.

init.lua — A Clean Entry Point

init.lua should be as small as possible: load the core configuration and bootstrap lazy.nvim, then let the other modules do their work. Never cram big logic here.

init.lua
-- Entry point: muat modul config inti
require("config.env")
require("config.options")
require("config.keymaps")
require("config.autocmds")
 
-- Bootstrap lazy.nvim & seluruh plugin (episode 9)
require("config.lazy")

lazy.nvim Bootstrap with Reproducibility

config/lazy.lua is the heart of it all. Notice how this script is self-bootstrapping — downloading lazy.nvim if it does not exist — so it can run on a new machine without extra manual steps (supporting the automatic bootstrap from episode 24):

lua/config/lazy.lua
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
  vim.fn.system({
    "git",
    "clone",
    "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable",
    lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)
 
require("lazy").setup({
  spec = {
    { import = "plugins" },        -- auto-import semua lua/plugins/*.lua
  },
  install = { colorscheme = { "tokyonight" } },
  checker = { enabled = true },    -- notifikasi update tersedia
  performance = {
    rtp = {
      disabled_plugins = {
        "netrwPlugin", "gzip", "zipPlugin", "tarPlugin", "tohtml", "matchit",
      },
    },
  },
})

Tip

spec = { { import = "plugins" } } makes lazy.nvim automatically load all files in lua/plugins/ as specs. Adding a new plugin is just creating one new file — no need to touch init.lua at all. This is why the modular structure from episode 9 keeps being used to this day.

Core Modules: Options, Keymaps, and Autocmds

These three modules are the foundation we built in episodes 7 and 8. options.lua controls editor behavior, keymaps.lua controls shortcuts, and autocmds.lua controls automatic reactions to events. Here is the "production-grade" version — complete with the best practices from the entire series:

lua/config/options.lua
local env = require("config.env")
 
-- 1. Tampilan & navigasi (episode 7)
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.cursorline = true
vim.opt.scrolloff = 8
vim.opt.signcolumn = "yes"
vim.opt.termguicolors = true
 
-- 2. Indentasi (episode 7)
vim.opt.tabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true
vim.opt.smartindent = true
 
-- 3. Pencarian (episode 7)
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.hlsearch = true
vim.opt.incsearch = true
 
-- 4. Keandalan & backup (episode 7)
vim.opt.undofile = true
vim.opt.undodir = env.undo_dir
vim.opt.swapfile = false
vim.opt.backup = false
vim.opt.clipboard = "unnamedplus"   -- sync dengan sistem clipboard
 
-- 5. Perilaku modern
vim.opt.mouse = "a"
vim.opt.splitright = true
vim.opt.splitbelow = true
vim.opt.completeopt = "menu,menuone,noselect"
vim.opt.wrap = false
vim.opt.updatetime = 250
vim.opt.timeoutlen = 300
 
-- 6. Direktori undo dibuat otomatis
vim.fn.mkdir(env.undo_dir, "p")
lua/config/keymaps.lua
-- Leader key: spasi (episode 7)
vim.g.mapleader = " "
 
local map = vim.keymap.set
 
-- Menyimpan & keluar (refleks dasar)
map("n", "<leader>w", "<cmd>w<CR>", { desc = "Save file" })
map("n", "<leader>q", "<cmd>q<CR>", { desc = "Quit window" })
 
-- Navigasi split (episode 5)
map("n", "<C-h>", "<C-w>h", { desc = "Go to left window" })
map("n", "<C-j>", "<C-w>j", { desc = "Go to lower window" })
map("n", "<C-k>", "<C-w>k", { desc = "Go to upper window" })
map("n", "<C-l>", "<C-w>l", { desc = "Go to right window" })
 
-- Buffer navigation (episode 5)
map("n", "<Tab>", "<cmd>bnext<CR>", { desc = "Next buffer" })
map("n", "<S-Tab>", "<cmd>bprevious<CR>", { desc = "Previous buffer" })
map("n", "<leader>bd", "<cmd>bdelete<CR>", { desc = "Close buffer" })
 
-- Mencari (episode 7)
map("n", "<Esc>", "<cmd>noh<CR>", { desc = "Clear search highlight" })
 
-- Meninggalkan insert mode dengan cepat
map("i", "jk", "<Esc>", { desc = "Exit insert mode" })
map("i", "jj", "<Esc>", { desc = "Exit insert mode" })
 
-- Jangan menimpa yank saat paste di visual mode (episode 3)
map("v", "p", '"_dP', { desc = "Paste without overwriting register" })
lua/config/autocmds.lua
local group = vim.api.nvim_create_augroup("user_config", { clear = true })
 
-- Kembali ke posisi terakhir saat membuka file (episode 8)
vim.api.nvim_create_autocmd("BufReadPost", {
  group = group,
  callback = function()
    local mark = vim.api.nvim_buf_get_mark(0, '"')
    if mark[1] > 1 and mark[1] <= vim.api.nvim_buf_line_count(0) then
      pcall(vim.api.nvim_win_set_cursor, 0, mark)
    end
  end,
})
 
-- Aktifkan wrap hanya untuk file markdown (episode 8)
vim.api.nvim_create_autocmd("FileType", {
  group = group,
  pattern = { "markdown", "text" },
  callback = function()
    vim.opt_local.wrap = true
  end,
})

LSP, Completion, and Formatting — The IDE Trio

These three modules are what make Neovim feel like a real IDE. lsp.lua connects Neovim to language servers (episode 15), cmp.lua provides autocompletion (episode 16), and conform.lua handles automatic formatting (episode 17):

lua/plugins/lsp.lua
{
  "neovim/nvim-lspconfig",
  event = { "BufReadPre", "BufNewFile" },
  dependencies = {
    { "williamboman/mason.nvim", cmd = "Mason", build = ":MasonUpdate" },
    "williamboman/mason-lspconfig.nvim",
    "hrsh7th/cmp-nvim-lsp",
  },
  config = function()
    require("mason").setup()
    require("mason-lspconfig").setup({
      ensure_installed = { "gopls", "pyright", "tsserver", "lua_ls", "dockerls" },
    })
 
    local capabilities = require("cmp_nvim_lsp").default_capabilities()
 
    local on_attach = function(_, bufnr)
      local bufmap = function(keys, fn, desc)
        vim.keymap.set("n", keys, fn, { buffer = bufnr, desc = desc })
      end
      bufmap("gd", vim.lsp.buf.definition, "Go to definition")
      bufmap("gr", vim.lsp.buf.references, "References")
      bufmap("K", vim.lsp.buf.hover, "Hover documentation")
      bufmap("<leader>rn", vim.lsp.buf.rename, "Rename symbol")
      bufmap("<leader>ca", vim.lsp.buf.code_action, "Code action")
      bufmap("<leader>e", vim.diagnostic.open_float, "Show diagnostics")
    end
 
    require("mason-lspconfig").setup_handlers({
      function(server)
        require("lspconfig")[server].setup({ on_attach = on_attach, capabilities = capabilities })
      end,
    })
  end,
}
lua/plugins/cmp.lua
{
  "hrsh7th/nvim-cmp",
  event = "InsertEnter",
  dependencies = {
    "hrsh7th/cmp-nvim-lsp",
    "hrsh7th/cmp-buffer",
    "hrsh7th/cmp-path",
    "L3MON4D3/LuaSnip",
    "saadparwaiz1/cmp_luasnip",
    "rafamadriz/friendly-snippets",
  },
  config = function()
    local cmp = require("cmp")
    local luasnip = require("luasnip")
    require("luasnip.loaders.from_vscode").lazy_load()
 
    cmp.setup({
      snippet = {
        expand = function(args) luasnip.lsp_expand(args.body) end,
      },
      mapping = cmp.mapping.preset.insert({
        ["<C-b>"] = cmp.mapping.scroll_docs(-4),
        ["<C-f>"] = cmp.mapping.scroll_docs(4),
        ["<C-Space>"] = cmp.mapping.complete(),
        ["<CR>"] = cmp.mapping.confirm({ select = true }),
        ["<Tab>"] = cmp.mapping(function(fallback)
          if cmp.visible() then cmp.select_next_item()
          elseif luasnip.expand_or_jumpable() then luasnip.expand_or_jump()
          else fallback() end
        end, { "i", "s" }),
      }),
      sources = cmp.config.sources({
        { name = "nvim_lsp" },
        { name = "luasnip" },
        { name = "buffer" },
        { name = "path" },
      }),
    })
  end,
}
lua/plugins/conform.lua
{
  "stevearc/conform.nvim",
  event = "BufWritePre",           -- format saat akan menyimpan
  config = function()
    require("conform").setup({
      formatters_by_ft = {
        lua = { "stylua" },
        python = { "ruff_fix", "ruff_format" },
        go = { "gofmt", "goimports" },
        javascript = { "prettierd", "prettier" },
        typescript = { "prettierd", "prettier" },
        ["*"] = { "trim_whitespace" },
      },
      format_on_save = {
        timeout_ms = 1500,
        lsp_fallback = true,
      },
    })
  end,
}

Note

Notice the lazy-loading patterns on all three: LSP loads at BufReadPre/BufNewFile (the buffer is open before we even get to interact), completion loads at InsertEnter (only needed once you start typing), and the formatter loads at BufWritePre (only needed when about to save). This is the application of the lazy-loading discipline from episode 23 that keeps 50+ plugins from ever blocking startup.

Daily-Driver Readiness Checklist

Before a config deserves to be called a daily driver, it must pass this entire checklist. Adapted from the lessons of the whole series:

NoAreaItemEpisode ReferenceStatus
1PerformanceStartup time < 100ms (target < 50ms)23
2Lazy-loadingAll non-essential plugins have a trigger (event/cmd/keys/ft)23
3Backupundofile enabled + undo directory created automatically7, 24
4SecurityNo secrets in config; .env.local gitignored24
5Gitgitsigns gutter + blame + diffview for conflicts18
6Debuggingnvim-dap + nvim-dap-ui + per-language debuggers22
7Testingneotest + adapters for the languages you use26
8AICopilot/Codeium/Avante configured (optional)21
9LSPmason + lspconfig + keymaps gd/gr/K/<leader>rn15
10Completionnvim-cmp + snippets (LuaSnip + friendly-snippets)16
11Formattingconform format-on-save for all main languages17
12Lintingnvim-lint async + diagnostics navigation [d/]d17
13Navigationtelescope (<leader>ff, <leader>fg) + oil11, 12
14Terminaltoggleterm + integrated lazygit19
15PortabilityConfig works on Linux + macOS + Windows from one repo24
16BootstrapIdempotent install script for new machines24
17Reproducibilitylazy-lock.json committed9

Important

This checklist is a gate, not an aspiration. A config that has not passed it does not yet deserve to be called production-grade. And remember: the performance and lazy-loading items should be maintained by habit, not just intention — every new plugin must answer the question "when is this plugin actually needed?" from episode 23.

Best Practices Accumulated Across 27 Episodes

Before the recap, let's summarize the best practices scattered across the series into one list you can hold onto:

#Best PracticeWhy
1Always modular: one file per pluginEasy to debug, easy to disable (episode 9)
2Lazy-load everything, without exceptionStartup stays under 50ms even with 50+ plugins (episode 23)
3Master text objects & motions first, plugins secondA strong foundation is worth more than fancy plugins (episode 3)
4Learn :help before asking the internetNeovim has outstanding documentation; the answer is often there
5Build workflows on top of Gitgitsigns, diffview, lazygit — versioned code is safe (episode 18)
6Test every config changeIf it breaks, Git enables fast rollback (episode 24)
7Keep secrets out of the repoSecurity is not a feature, but an absolute requirement (episode 24)
8Measure before optimizing--startuptime and :Lazy profile before touching the config (episode 23)
9Be consistent, do not change keymaps every weekMuscle memory needs stability (episode 25)
10Build your own configUnderstanding is an asset you cannot buy from a distro (episode 25)

Journey Recap: From Episode 0 to Episode 27

Let's look at the big map you have traveled together. Six phases, twenty-eight episodes — from merely getting to know modal editing to building a production-grade work environment:

PhaseEpisodesCore Material
Foundation & Modal Editing0–4Environment setup, Neovim history, modal editing, text objects, registers & macros
Windows, Buffer & Lua5–9Window/buffer management, migration to Lua, options & keymaps, autocmds, lazy.nvim
Visualization & Navigation10–13Colorscheme & statusline, telescope, file explorer, flash & session management
Modern IDE Capabilities14–18Treesitter, LSP & mason, completion, formatting & linting, Git integration
Advanced Workflow & AI19–22Terminal & task management, editing productivity, AI assistant, debugging with DAP
Performance, Distribution, Testing23–26Profiling & advanced lazy-loading, dotfiles & portability, distro vs custom, neotest
Production Readiness27Production-grade config architecture & daily-driver checklist

From episode 0, still wondering "what are modal editing skills", you can now design a work environment that is fast, reliable, portable, and accountable — complete with LSP, completion, formatting, testing, debugging, and AI, all inside an editor that runs in the terminal. That is a capability few engineers have, and now you have it.

Closing

Congratulations — you have completed the Learn Neovim series from episode 0 to episode 27!

Let's take a moment to look back. In episode 27 we assembled everything into one production-grade config architecture: a clean modular structure, a minimal init.lua, a reproducible lazy.nvim bootstrap, solid options/keymaps/autocmds modules, the LSP-completion-formatting trio that turns Neovim into an IDE, as well as the daily-driver readiness checklist and best practices that unite the whole series' lessons.

But the most valuable thing is not the code — it is the way of thinking you now have:

  1. Foundation first, plugins later. You learned motions and text objects before plugins — so plugins only accelerate what you already master.
  2. Understand before automating. You learned options, keymaps, and autocmds from their roots — so there is no "magic" you cannot explain.
  3. Measure and optimize with data. Low startup time is not luck, but the result of measurement and lazy-loading discipline.
  4. Build your own, do not buy. Your config is a mirror of how you think — and building it yourself is the best way to truly own it.
  5. Make it reliable and portable. Git, undofile, dotfiles, and bootstrap — a reproducible environment is a trustworthy environment.

Your journey does not stop here. Some next steps you can take to keep growing:

  • Build your own config from scratch. Use the episode 27 structure as a map, but fill it with your own decisions. Start small, then grow it.
  • Read other people's configs. GitHub is full of great public dotfiles — learn how other engineers solved the same problems, as we discussed in episodes 24 and 25.
  • Study :help in depth. Neovim is a tool with nearly endless depth; the built-in documentation is your advanced curriculum.
  • Contribute back to the community. Write tutorials, share your config, or help answer questions on Neovim forums. Teaching is the best way to master something.
  • Keep exploring the ecosystem. Lua scripting in Neovim, making your own plugin, or integrating Neovim with other tooling — the sky is not the limit.

Remember the lesson from episode 1: Neovim is an editor with the philosophy of thinking speed in parallel with typing speed. You have traveled 28 episodes to reach the point where the editor no longer blocks you — it becomes an extension of your hand. A skill that never goes out of date: editors will never be replaced, and the way you use yours is a competitive advantage that keeps compounding every day.

Thank you for accompanying this journey to the end. Now — open your terminal, write your own config, and let every keypress become a reflex that brings you closer to the workflow you dream of. Happy coding, and see you in the terminal!

Learn Neovim - Complete Production-Grade Neovim Setup & Best Practices | Learn Neovim