NGINX is famous for serving static content and handling reverse proxies — but it can do a lot more. In this post,
we’re going to explore three “power moves” with NGINX:
Running a reverse proxy to local services
Embedding Lua scripts using OpenResty
Talking to local services over Unix domain sockets
By the end, you’ll be able to glue together web services like a pro — no Node.js middleman required.
Setup
Before we begin, we’ll setup your local development environment so that you can experiment with a few of these
configurations. We’ll use docker and the OpenResty image to simplify
our setup.
I’ve created a directory structure that looks like this:
This setup allows you to do things like offload your SSL/TLS onto NGINX rather than needing to deal with it inside of
your application. You’re actually controlling the flow of traffic using this reverse proxy setup, so it will make your
overall system design a lot more flexible should you need to pivot in future.
I put this configuration into a file called ./conf.d/basic.conf. I run this with the following:
docker run --rm-it\ -p 8080:8080 \-v"$PWD/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro"\-v"$PWD/conf.d/basic.conf:/etc/nginx/conf.d/default.conf:ro"\
openresty/openresty:alpine
Sending a curl request should fail (if you’re like me and you don’t have a service running on port 5000):
curl http://localhost:8080/api/test
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>openresty/1.27.1.2</center>
</body>
</html>
This is all well and good for basic reverse proxying. What if you need a little more functionality at your proxy? You
may want some arbitrary logic. In order to do this, you need some more help.
Lua + OpenResty: Custom Logic at the Edge
Want conditional logic? Inline transformations? Run Lua scripts inside NGINX using OpenResty.
Here’s an example that rewrites responses:
location /hello {
content_by_lua_block {
ngx.say("Hello from Lua!")
}
}
Save this into ./conf.d/lua.conf and then ee can get this running with the following:
docker run --rm-it\ -p 8080:8080 \-v"$PWD/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro"\-v"$PWD/conf.d/lua.conf:/etc/nginx/conf.d/default.conf:ro"\
openresty/openresty:alpine
A simple request to the /hello endpoint:
curl http://localhost:8080/hello
Hello from Lua!
This demonstrates the usage of content_by_lua_block to provide content to the response.
Or maybe you want to inspect a header before proxying:
location /auth {
access_by_lua_block {
local token = ngx.var.http_authorization
if token ~= "Bearer secrettoken" then
ngx.status = 401
ngx.say("Unauthorized")
return ngx.exit(401)
end
}
proxy_pass http://localhost:7000/;
}
The socket is created by the host user, but inside the container, NGINX runs as a different user (typically nobody).
To avoid a “permission denied” error, I made the socket world-accessible:
docker run --rm-it\-p 8080:8080 \-v"$PWD/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro"\-v"$PWD/conf.d/unix.conf:/etc/nginx/conf.d/default.conf:ro"\-v"/tmp/mysock:/tmp/mysock"\
openresty/openresty:alpine
You can see that we’ve needed to mount in our socket.
Wrap-up
NGINX isn’t just a dumb proxy — with OpenResty and some careful configuration, it becomes a programmable router and
request gatekeeper. If you’re building internal tools, APIs, or secure microservices — this setup is gold.
Continuations are one of those ideas that seem abstract at first — but once they click, you’ll start seeing them
everywhere: in asynchronous code, in exception handling, in generators, even in the way you reason about program flow.
In this post, we’ll explore what continuations are, how Continuation-Passing Style (CPS) transforms your code, and how
these concepts are quietly powering modern async constructs like async/await.
We’ll walk through synchronous code, asynchronous patterns with callbacks and promises, and finally reach a new
understanding of what’s really going on under the hood. You won’t need to reimplement a language or build a compiler
to follow along — we’ll do everything with regular JavaScript, Python, and Rust.
What is a Continuation?
A continuation is a representation of “what to do next” in a program. At any point in your code, the rest of the
computation can be thought of as a function — the continuation.
Let’s start simple:
functionaddOne(x){returnx+1;}console.log(addOne(2));// prints 3
Now, instead of returning, what if we passed the result to another function — the continuation?
functionaddOneCPS(x,cont){cont(x+1);}addOneCPS(2,(result)=>{console.log(result);// prints 3});
This style is called Continuation-Passing Style (CPS). In CPS, functions never return — they
call their continuation instead.
This isn’t immediately remarkable. Callbacks have been used widely in code for quite some time now. This is just
building a picture of where we’ve come from.
We’ve restructured our program so that each step explicitly passes control to the next.
This may seem verbose, but it turns out to be extremely powerful — especially when dealing with asynchronous code.
CPS is Everywhere in JavaScript
Let’s look at an example from the Node.js world:
constfs=require("fs");fs.readFile("data.txt","utf8",(err,data)=>{if(err)returnconsole.error("Failed to read file:",err);processData(data,(result)=>{console.log("Result:",result);});});
This is literally CPS — instead of returning data, fs.readFilepasses it to a callback.
What’s the callback? The continuation.
Promises: CPS with a Better API
JavaScript promises are built around the same idea, just cleaner:
But this isn’t “real” synchronous code — it just looks like it.
Behind the scenes, the JavaScript engine is:
Splitting the function into pieces at each await
Saving the continuation (the rest of the function) in a hidden state machine
Resuming that continuation when the awaited promise resolves
That’s CPS at work.
Manual CPS in Other Languages
You don’t need a JavaScript engine to try this out. Let’s look at Rust and Python examples to see how CPS can be
expressed in ordinary code.
Rust Example
fndouble_cps(x:i32,cont:implFnOnce(i32)){cont(x*2);}fnadd_one_cps(x:i32,cont:implFnOnce(i32)){cont(x+1);}fnmain(){double_cps(5,|doubled|{add_one_cps(doubled,|result|{println!("Result: {}",result);// prints 11});});}
Rust’s closures let us express continuations cleanly without needing async runtimes or macros.
Python Example
The same example can be implemented using python pretty simply.
Compilers often sound mysterious — full of dragons, jargon, and intimidating diagrams. But under the hood, they all
follow the same basic blueprint.
In this post, we’ll walk through every major compiler phase by implementing them in Rust. We’ll start with a raw
source string and end with real output — step by step — using this ultra-simple language:
let x = 1 + 2;
We’ll transform that line through tokenization, parsing, type checking, intermediate representation, optimization,
and finally evaluation — all in plain Rust. Every phase will be practical, with real code you can run. By the end,
you’ll have a minimal but complete compiler front-end — a solid foundation you can build on.
Here’s a high-level view of the journey:
graph TD
A["Source Code"] --> B["Lexical Analysis (Tokenizer)"]
B --> C["Parsing (AST Builder)"]
C --> D["Semantic Analysis (Type Checking)"]
D --> E["IR Generation (Lowering to Intermediate Form)"]
E --> F["Optimization (Constant Folding, etc.)"]
F --> G["Code Generation (Assembly or Interpreter)"]
G --> H["Final Output (Binary or Result)"]
Let’s dive in!
Lexical Analysis
Lexical analysis — also called tokenization or scanning — is the first phase of a compiler. It takes a stream
of raw characters (your source code) and breaks it into tokens: the smallest meaningful units of the language.
Tokens include things like keywords (let), identifiers (x), operators (+), and literals (1, 2). Once
tokenized, the compiler can start to reason about structure instead of individual characters.
You can see here that there’s no real validation going on here. We’re just turning significant characters into tokens.
The only piece of validation that does occur is if a character isn’t supported by the process, we’ll panic.
Parsing
Now that we have a stream of tokens, it’s time to make sense of them structurally. That’s the job of parsing —
converting a flat list of tokens into a tree-like structure that represents the hierarchy and meaning of the
code.
This structure is called an Abstract Syntax Tree (AST).
An AST is a simplified, structured representation of your source code that captures its grammatical structure —
without worrying about superficial syntax details like commas, parentheses, or whitespace. It lets the compiler
understand what the code means rather than just what it looks like.
Taking our simple example from above, we want to produce a tree that looks like this:
The parser turns our flat tokens into this rich structure — making it possible for later phases (like type checking
and code generation) to analyze, transform, and ultimately execute the code.
The expressions that our parser will support can be a literal value, or it can be a binary operation. You’ll notice
that the binary operation is also self-referencing Expr.
Once we have an Abstract Syntax Tree (AST), we know how the code is structured. But we still don’t know if the code
makes sense.
That’s where semantic analysis comes in. This phase checks the meaning of the code — validating things like:
Are all variables declared before they’re used?
Are types used correctly?
Can the operations actually be performed?
Even though let x = 1 + 2; is syntactically valid, we still need to ensure the types on both sides of + are
compatible, and that x is a valid target for assignment.
Semantic analysis walks the AST and performs:
Type checking (e.g., can you add these two things?)
Scope resolution (e.g., is this variable declared?)
Error reporting for violations (e.g., type mismatches)
We need to tell our compiler about types.
#[derive(Debug,PartialEq)]enumType{Int,}
Now we can lean on the recursive nature of our Expr struct to process it recursively. Very simply, we only support
one type: Int.
At this point, we have code that’s both structurally valid (parsed into an AST) and semantically correct
(passes type checking). Now it’s time to lower that code into something simpler and easier to manipulate: an
Intermediate Representation, or IR.
Think of IR as the “compiler’s private language” — a stripped-down version of your program that’s:
Easier to optimize
Easier to analyze
Easier to transform into real machine instructions
In real-world compilers, IR might take the form of:
Once we have our Intermediate Representation (IR), we can start to improve it.
That’s what the optimization phase is all about — rewriting the IR to make it:
Faster to run
Simpler to execute
More efficient in terms of operations
Crucially, optimizations don’t change the meaning of the program. They just make it better.
In our toy compiler, we’ll implement a classic example: constant folding. This is when the compiler evaluates
constant expressions ahead of time.
Instead of this:
LoadConst 1
LoadConst 2
Add
Store x
We generate this:
LoadConst 3
Store x
That means less work at runtime — and it’s a stepping stone to more advanced techniques like dead code elimination,
common subexpression elimination, or register allocation.
Even in small compilers, optimization is important because:
It reduces unnecessary instructions
It prepares the IR for efficient code generation
It gives you experience with real-world compiler passes
In this section, we’ll:
Walk through our IR instructions
Detect simple constant patterns
Replace them with pre-computed values
The logic will be basic — but the mechanism will mirror what real compilers do at massive scale.
In a previous post, we built a dynamic variant type in C
that could safely hold integers, floats, strings, and arrays — all while tracking its current type.
That implementation used a simple, effective trick from the C toolbox: the tagged union.
But what is a tagged union, really? What does it look like in memory? How does it behave across different languages
like Rust or Swift? And how can understanding this help you write better low-level code?
In this article, we’ll go deep into the mechanics of tagged unions — demystifying how they work, how they’re laid out
in memory, and how other languages leverage this idea at a higher level.
What is a Tagged Union?
A tagged union is a structure that lets you store different types of data in the same memory space — but only
one of them at a time.
It consists of two parts:
A tag (also called a discriminator or type id) — this tells you what kind of value the union currently holds.
A union — this is a single memory region that can represent multiple types, but only one is valid at any given time.
Together, they form a tagged union, also known as:
Variant type (C++)
Discriminated union (TypeScript)
Sum type (Haskell, Rust)
enum with payloads (Rust, Swift, F#)
A Simple C Implementation
Let’s take a minimal version of the ced_var_t type we saw earlier:
When you store a value, you must set both correctly:
tagged_value_tv;v.type=TYPE_INT32;v.data.i32=42;
When you want to read the value, you must check the tag first.
What Does This Look Like in Memory?
Let’s visualize what’s in memory for the above:
+--------+---------+------------------+
| tag | padding | data |
| (4B) | (4B) | (8B - union) |
+--------+---------+------------------+
Because str is a char*, which is typically 8 bytes on 64-bit systems, the entire union needs to reserve
8 bytes. So the full struct is:
4 bytes for type (as “tag”)
4 bytes padding (for alignment)
8 bytes for union
Total: 16 bytes
Even if you’re only storing a small int32_t, the full memory block remains the same.
Keep in mind! This is a classic example of how low-level memory alignment and padding rules affect real-world struct layout — and why tools like sizeof() sometimes surprise you.
Why Is This Useful?
Tagged unions give you flexibility:
Store different types dynamically
Inspect and branch on their type at runtime
Compact representation (compared to void* + metadata hacks)
Zero-cost abstraction in low-level code
They’re great for interpreters, configuration systems, scripting languages, or anything where types can vary
dynamically.
But It’s Also Dangerous
C won’t stop you from misusing the union:
v.type=TYPE_FLOAT;v.data.i32=1234;// Invalid — tag and payload don't match!
Accessing a union member that doesn’t match the tag is undefined behavior. It might work. Or it might crash. Or
silently corrupt your data.
That’s why you must manage the tag manually, and treat it as the single source of truth.
How Rust Does It Better
In Rust, you’d write this as:
enumValue{Int32(i32),Float(f32),Str(String),}
Rust does three major things better:
Enforces valid construction — you can’t create an invalid state.
Requires match exhaustiveness — you have to handle all cases.
Uses niche optimization — it can sometimes omit the tag entirely.
For example, Option<&T> takes no extra space. Rust uses the null pointer as the tag for None.
Comparison with Other Languages
Language
Feature
Example
C
Tagged union (manual)
struct + union + enum
Rust
Sum type with safety
enum Value { Int(i32), ... }
Swift
enum with associated data
enum Value { case int(Int), ... }
TypeScript
Discriminated union
{ kind: "int", value: number }
Haskell/OCaml
Algebraic data type
data Value = Int Int | Float Float
Performance Considerations
Tagged unions are generally compact, but there are trade-offs:
The size of the union is dictated by the largest member.
On 64-bit platforms, alignment often causes padding (e.g., 4-byte tag + 4-byte pad + 8-byte payload).
If a variant holds something heap-allocated (like strings), you may incur pointer-chasing and memory fragmentation.
You can use tools like sizeof(), offsetof(), or pahole to understand the exact layout.
Debugging Tips
If you’re building or debugging a tagged union system in C:
Use assert() to guard tag-to-union access:
assert(v->type==TYPE_INT32);
Use valgrind to catch misuses or memory leaks.
Inspect raw memory using gdb, hexdump, or print helpers.
Consider printing the tag as a string to make logs easier to read.
Rust is famous for giving you memory safety without a garbage collector. But when you start doing lower-level work —
self-referential structs, async state machines, or FFI — you run into a powerful but mysterious feature: Pin.
In this article, we’ll answer the following:
What does it mean for a type to be pin-safe?
Why would a type need to be pinned in the first place?
How do you build one safely — without fighting the borrow checker?
We’ll walk through simple examples first, then build up to a self-referential type.
What is a Pin-safe Type?
A pin-safe type is a type that can be safely used with Rust’s Pin API.
Pin: It promises not to move itself in memory after being pinned, and it uses unsafe code responsibly to uphold that guarantee.
You create a Pin-safe type when:
You need to guarantee that a value won’t move in memory after being created.
You want to allow self-referencing inside a struct (e.g., a field pointing to another field).
You’re building async state machines, generators, or intrusive data structures.
Self-Referential Structs: The Core Problem
Let’s look at a classic case. Say you have a struct like this:
structExample{a:String,b:*constString,// b points to a}
This is a self-referential struct: b stores a pointer to another field inside the same struct.
Seems harmless?
Here’s the catch: Rust moves values around freely — into function calls, collections, etc. If you set up a pointer
inside a struct and then move the struct, your pointer is now invalid. This opens the door to use-after-free bugs.
Rust’s borrow checker normally prevents you from doing this. But sometimes you do need this — and that’s where Pin
comes in.
Pin to the Rescue
Pin<T> says Once this value is pinned, it cannot be moved again.
This is perfect for self-referential types — it guarantees their memory address won’t change.
But you have to build your type carefully to uphold this contract.
A Pin-safe Self-Referential Type
Now let’s build a Pin-safe type step-by-step.
Step 1: Define the structure
usestd::pin::Pin;usestd::marker::PhantomPinned;structSelfRef{data:String,data_ref:*constString,// raw pointer, not a safe Rust reference_pin:PhantomPinned,// opt-out of Unpin}
data: holds some content.
data_ref: stores a pointer to that data.
PhantomPinned: tells Rust this type is not safe to move after being pinned.