programming4 min read

Tutorial: Learn Nim from Scratch (2026)

Tutorial: Learn Nim from Scratch (2026)

Published:  |  Category: Programming  |  Reading time: ~15 min
Tutorial: Learn Nim from Scratch (2026)

Nim is a statically typed systems language with Python-like indentation-based syntax. I have used it for CLI tools, game development, and web applications. The language compiles to C, C++, or JavaScript, giving you the performance of C with the expressiveness of a modern language.

Nim's metaprogramming capabilities are extraordinary. The AST is accessible at compile time, letting you write macros that transform code as naturally as processing data.

Metaprogramming

Nim macros operate on the AST at compile time. They receive NimNode objects and return transformed AST. This allows creating DSLs, code generation, and compile-time optimizations. Templates are simpler than macros: they do textual substitution with parameter injection. Static procedures run at compile time with static blocks.

import macros

macro assert(cond: untyped): untyped =
  result = quote do:
    if not `cond`:
      raise newException(AssertionError,
        "Assertion failed: " & `cond`.toStrLit.strVal)

assert(2 + 2 == 4)

template measure(body: untyped): untyped =
  let start = cpuTime()
  body
  echo "Elapsed: ", cpuTime() - start

Types and Generics

Nim's type system includes ordinals, enums, sets, objects, tuples, and variants (with case). Generics use square brackets: proc foo[T](x: T). Concepts constrain generic types with requirements. Objects can be either ref (heap, garbage collected) or value types on the stack.

type
  Color = enum cRed, cGreen, cBlue
  
  Person = object
    name: string
    age: int
  
  JsonNode = ref object
    case kind: JsonKind
    of jString: strVal: string
    of jNumber: numVal: float
    of jArray: children: seq[JsonNode]

proc first[T](s: seq[T]): T =
  s[0]

let nums = @[1, 2, 3]
echo first(nums)

Performance

Nim compiles to C with no runtime overhead in critical paths. You can use {.pure.}, {.packed.}, and {.align.} pragmas for fine control. The GC is optional — you can use --gc:arc or --gc:orc for deterministic reference counting. The noGC option disables GC entirely for hard real-time systems.

type Vec3 = object
  x, y, z: float32

{.push pure, align: 16.}
  type Matrix = array[4, Vec3]
{.pop.}

proc dot(a, b: Vec3): float32 {.inline.} =
  a.x * b.x + a.y * b.y + a.z * b.z

proc matVecMul(m: Matrix, v: Vec3): Vec3 =
  result.x = dot(m[0], v)
  result.y = dot(m[1], v)
  result.z = dot(m[2], v)

Macros

Nim's macro system gives full access to the AST. dumpTree prints AST for debugging. newStmtList, newCall, newIdentNode build AST nodes programmatically. quote do: creates AST from Nim code. Slots (backtick) interpolate nodes. Macros can generate types, procedures, and even modify the compiler's behavior.

import macros

macro enumToString(T: typedesc): untyped =
  result = newStmtList()
  for sym in T.getType[1..^1]:
    let name = sym.strVal
    result.add quote do:
      proc `name`(x: `T`): bool =
        x == `sym`

macro dsl(body: untyped): untyped =
  body.expectKind(nnkStmtList)
  for child in body:
    child.expectKind(nnkCommand)
    echo child[0].repr, " -> ", child[1].repr

Concurrency

Nim uses threads with shared memory protected by channels and locks. The standard library provides threadpool for a spawn/sync model. For async I/O, Nim has asyncmacro and asyncdispatch similar to Python's asyncio. The spawn statement executes a procedure on a thread pool thread.

import threadpool

proc process(i: int): int =
  result = i * i

let responses = collect:
  for i in 0..<100:
    spawn process(i)

sync()
for r in responses:
  echo r

import asyncdispatch

proc fetch(url: string): Future[string] {.async.} =
  let client = newAsyncHttpClient()
  result = await client.getContent(url)

FFI

Nim calls C and C++ directly with no wrappers needed. The {.importc.} pragma binds to C functions. {.header.} includes C headers. {.nodecl.} suppresses declaration generation. For C++ use {.importcpp.}. Nim can also compile to JavaScript for frontend code, sharing types and procedures between client and server.

proc printf(frmt: cstring): cint {.importc, varargs, header: "".}

proc strlen(s: cstring): csize_t {.importc, header: "".}

printf("Hello from Nim! pi = %.2f\n", 3.14159)

# Export to C
proc myFunction(x: cint): cint {.exportc.} =
  x * 2

Frequently Asked Questions

Nim vs Python?

Nim compiles to native code, has static types, and runs orders of magnitude faster. Python has a larger ecosystem. Syntax is similar.

Is Nim garbage collected?

Yes by default, but you can choose --gc:arc (reference counting), --gc:orc (cycle collector), or --gc:none (manual).

What is the difference between var and let?

var declares mutable variables. let declares immutable variables. const is compile-time constant.

How does Nim handle null?

Nim does not have null. Use Option[T] from the standard library for optional values. References are non-nil by default.

Originally published on Ayodhyyya. Last updated June 1, 2026.