OpenResty Coding Style Guide

API7.ai

December 15, 2022

OpenResty (NGINX + Lua)

Many development languages have their coding specifications to tell developers some conventions in the field, to keep the style of code written consistent, and to avoid some common pitfalls. Python's PEP 8 is an excellent example of this, and almost all Python developers have read this coding specification written by Python's authors.

OpenResty doesn't have its coding specification yet, and some developers are repeatedly reviewed and asked to change their code style after submitting PRs, which consumes a lot of time and effort that could be avoided.

There are two Lint tools in OpenResty that can help you detect code style automatically: luacheck and lj-releng. The former is a common Lint tool in Lua and OpenResty world, and the latter is a Lint tool written in Perl by OpenResty itself.

For myself, I install the luacheck plugin in the VS Code editor so that I have a tool to auto-suggest when I write code; and in the CI of a project, I run both tools, e.g.

luacheck -q lua

./utils/lj-releng lua/*.lua lua/apisix/*.lua

After all, one more tool for testing is never a bad thing.

However, these two tools are more about detecting global variables, length per line, and other most basic code styles, which are still far from the detailed level of Python PEP 8, and there is no documentation for you to refer to.

So today, based on my experience in OpenResty-related open-source projects, I've summarized the OpenResty coding style documentation. This specification is also consistent with the code style of some API gateways like APISIX and Kong.

Indentation

In OpenResty, we use 4 spaces as indentation markers, although Lua does not require such syntax. Here are two examples of incorrect and correct codes.

--No
if a then
ngx.say("hello")
end
--yes
if a then
    ngx.say("hello")
end

For convenience, we can simplify the operation by changing the tab to 4 spaces in the editor you are using.

Space

On both sides of the operator, a space is needed to separate them. The following are two examples of incorrect and correct codes.

--No
local i=1
local s    =    "apisix"
--Yes
local i = 1
local s = "apisix"

Blank line

Many developers bring development conventions from other languages to OpenResty, such as adding a semicolon to the end of a line:

--No
if a then
    ngx.say("hello");
end;

But in fact, adding semicolons makes Lua code look very ugly, which is unnecessary. Also, you should not turn multiple lines of code into a single line to save lines to be concise. Doing so will leave you with no idea which section of code is at fault when you locate the error.

--No
if a then ngx.say("hello") end
--yes
if a then
    ngx.say("hello")
end

In addition, the functions need to be separated by two blank lines.

--No
local function foo()
end
local function bar()
end
--Yes
local function foo()
end


local function bar()
end

If there are multiple if elseif branches, they also need to be separated by a blank line.

--No
if a == 1 then
    foo()
elseif a== 2 then
    bar()
elseif a == 3 then
    run()
else
    error()
end
--Yes
if a == 1 then
    foo()

elseif a== 2 then
    bar()

elseif a == 3 then
    run()

else
    error()
end

Maximum length per line

Each line should not exceed 80 characters; if it exceeds that, we need to line break and align. And when aligning line breaks, we need to reflect the correspondence between the top and bottom lines. For the example below, the function's argument on the second line should be to the right of the left bracket on the first line.

--No
return limit_conn_new("plugin-limit-conn", conf.conn, conf.burst, conf.default_conn_delay)
--Yes
return limit_conn_new("plugin-limit-conn", conf.conn, conf.burst,
                    conf.default_conn_delay)

If it is the alignment of splicing strings, we need to put .. in the next line.

--No
return limit_conn_new("plugin-limit-conn" ..  "plugin-limit-conn" ..
                    "plugin-limit-conn")
--Yes
return limit_conn_new("plugin-limit-conn" .. "plugin-limit-conn"
                    .. "plugin-limit-conn")

Variable

This point was also emphasized several times in previous articles: we should always use local variables rather than global variables.

--No
i = 1
s = "apisix"
--Yes
local i = 1
local s = "apisix"

As for the naming of variables, the snake_case style should be used.

--No
local IndexArr = 1
local str_Name = "apisix"
--Yes
local index_arr = 1
local str_name = "apisix"

For constants, on the other hand, the all-caps style is to be used.

--No
local max_int = 65535
local server_name = "apisix"

--Yes
local MAX_INT = 65535
local SERVER_NAME = "apisix"

Table

In OpenResty, we use table.new to pre-allocate the table.

--No
local t = {}
for i = 1, 100 do
   t[i] = i
 end
--Yes
local new_tab = require "table.new"
 local t = new_tab(100, 0)
 for i = 1, 100 do
   t[i] = i
 end

Also, note that you must not use nil in the array, and if you must use null, use ngx.null to indicate that.

--No
local t = {1, 2, nil, 3}

--Yes
local t = {1, 2, ngx.null, 3}

String

Never splice strings on a hot code path.

--No
local s = ""
for i = 1, 100000 do
    s = s .. "a"
end
--Yes
local t = {}
for i = 1, 100000 do
    t[i] = "a"
end
local s =  table.concat(t, "")

Function

The naming of functions also follows snake_case.

--No
local function testNginx()
end
--Yes
local function test_nginx()
end

And the function should return as early as possible.

--No
local function check(age, name)
    local ret = true
    if age < 20 then
        ret = false
    end

    if name == "a" then
        ret = false
    end
    -- do something else
    return ret
--Yes
local function check(age, name)
    if age < 20 then
        return false
    end

    if name == "a" then
        return false
    end
    -- do something else
    return true

Module

All required libraries must be localized:

--No
local function foo()
    local ok, err = ngx.timer.at(delay, handler)
end
--Yes
local timer_at = ngx.timer.at

local function foo()
    local ok, err = timer_at(delay, handler)
end

For consistency of style, require and ngx also need to be localized:

--No
local core = require("apisix.core")
local timer_at = ngx.timer.at

local function foo()
    local ok, err = timer_at(delay, handler)
end
--Yes
local ngx = ngx
local require = require
local core = require("apisix.core")
local timer_at = ngx.timer.at

local function foo()
    local ok, err = timer_at(delay, handler)
end

Error handling

For functions that return with error information, the error information must be judged and processed:

--No
local sock = ngx.socket.tcp()
local ok = sock:connect("www.google.com", 80)
ngx.say("successfully connected to google!")
--Yes
local sock = ngx.socket.tcp()
local ok, err = sock:connect("www.google.com", 80)
if not ok then
    ngx.say("failed to connect to google: ", err)
    return
end
ngx.say("successfully connected to google!")

And in the case of functions written by yourself, the error information is to be returned as a second parameter in string format:

--No
local function foo()
    local ok, err = func()
    if not ok then
        return false
    end
    return true
end
--No
local function foo()
    local ok, err = func()
    if not ok then
        return false, {msg = err}
    end
    return true
end
--Yes
local function foo()
    local ok, err = func()
    if not ok then
        return false, "failed to call func(): " .. err
    end
    return true
end

Summary

This is an initial version of the coding style guide, and we'll be making it available on GitHub for ongoing updates and maintenance. You are welcome to share this specification so that more OpenResty users can get involved.