Skip to content

Code Generators

The zzz gen command (short for zzz generate) creates boilerplate code from built-in templates. It supports four generator types: controllers, models, channels, and mailers.

Terminal window
zzz gen <type> <Name> [options...]

The <Name> argument is case-sensitive. By convention, pass it in PascalCase (e.g., Users, BlogPost). The CLI converts it to lowercase for file names.

Terminal window
zzz gen controller <Name>

Generates a RESTful controller at src/controllers/<name>.zig with five actions: index, show, create, update, and delete_handler. The file also exports a routes constant that maps these actions to standard REST paths using Router.resource.

Example:

src/controllers/users.zig
zzz gen controller Users

Generated code:

const std = @import("std");
const zzz = @import("zzz");
const Context = zzz.Context;
const Router = zzz.Router;
// Users Controller
pub fn index(ctx: *Context) !void {
ctx.json(.ok, "{\"message\":\"users index\"}");
}
pub fn show(ctx: *Context) !void {
const id = ctx.param("id") orelse "unknown";
_ = id;
ctx.json(.ok, "{\"message\":\"Users show\"}");
}
pub fn create(ctx: *Context) !void {
ctx.json(.created, "{\"message\":\"Users created\"}");
}
pub fn update(ctx: *Context) !void {
ctx.json(.ok, "{\"message\":\"Users updated\"}");
}
pub fn delete_handler(ctx: *Context) !void {
ctx.json(.ok, "{\"message\":\"Users deleted\"}");
}
pub const routes = Router.resource("/users", .{
.index = index,
.show = show,
.create = create,
.update = update,
.delete_handler = delete_handler,
});

To wire the controller into your app, import it in src/main.zig and add its routes:

const users = @import("controllers/users.zig");
const App = Router.define(.{
.routes = &.{
users.routes,
},
});
Terminal window
zzz gen model <Name> [field:type ...]

Generates two files:

  1. Schema at src/<name>.zig — a Zig struct with typed fields and a Meta constant for the database layer.
  2. Migration at priv/migrations/001_create_<name>.zigup and down functions for creating and dropping the table.

Every model automatically includes id, inserted_at, and updated_at fields.

Supported field types:

CLI typeZig typeSQL column type
string[]const u8text
text[]const u8text
integer or inti64bigint
float or realf64real
boolean or boolboolboolean

Example:

src/post.zig
zzz gen model Post title:string body:text user_id:integer published:boolean
# Generated: priv/migrations/001_create_post.zig

Generated schema (src/post.zig):

const schema = @import("zzz_db").Schema;
/// Post schema definition.
pub const Post = struct {
id: i64,
title: []const u8,
body: []const u8,
user_id: i64,
published: bool,
inserted_at: i64 = 0,
updated_at: i64 = 0,
pub const Meta = schema.define(@This(), .{
.table = "post",
.primary_key = "id",
.timestamps = true,
});
};

Generated migration (priv/migrations/001_create_post.zig):

const MigrationContext = @import("zzz_db").MigrationContext;
pub fn up(ctx: *MigrationContext) !void {
try ctx.createTable("post", &.{
.{ .name = "id", .col_type = .integer, .primary_key = true, .auto_increment = true },
.{ .name = "title", .col_type = .text, .nullable = false },
.{ .name = "body", .col_type = .text, .nullable = false },
.{ .name = "user_id", .col_type = .bigint, .nullable = false },
.{ .name = "published", .col_type = .boolean, .nullable = false },
.{ .name = "inserted_at", .col_type = .bigint, .nullable = false },
.{ .name = "updated_at", .col_type = .bigint, .nullable = false },
});
}
pub fn down(ctx: *MigrationContext) !void {
try ctx.dropTable("post");
}
Terminal window
zzz gen channel <Name>

Generates a WebSocket channel at src/channels/<name>.zig with a join handler and a message broadcast handler.

Example:

src/channels/chat.zig
zzz gen channel Chat

Generated code:

const std = @import("std");
const zzz = @import("zzz");
const Socket = zzz.Socket;
const ChannelDef = zzz.ChannelDef;
const JoinResult = zzz.JoinResult;
// Chat Channel
fn handleJoin(socket: *Socket, topic: []const u8, _payload: []const u8) JoinResult {
_ = socket;
_ = topic;
return .ok;
}
fn handleNewMessage(socket: *Socket, topic: []const u8, _event: []const u8, payload: []const u8) void {
socket.broadcast(topic, "new_msg", payload);
}
pub const channel = ChannelDef{
.topic_pattern = "chat:*",
.join = &handleJoin,
.handlers = &.{
.{ .event = "new_msg", .handler = &handleNewMessage },
},
};

The topic_pattern uses a wildcard (chat:*) so clients can join topic-specific rooms such as chat:lobby or chat:room_42.

Terminal window
zzz gen mailer <Name>
zzz gen mailer <Name> --template

Generates a mailer module at src/mailers/<name>.zig with a build function that constructs an Email struct.

Without --template, the email body is defined inline in Zig code. With --template, the generator creates separate template files:

  • src/mailers/templates/<name>.html.zzz — HTML email template
  • src/mailers/templates/<name>.txt.zzz — plain-text email template

The mailer module uses @embedFile to load these templates at compile time.

Example:

src/mailers/welcome.zig
zzz gen mailer Welcome

If a file already exists at the target path, the generator skips it and prints a message:

File already exists: src/controllers/users.zig (skipped)

This prevents accidental overwrites. Delete or rename the existing file first if you want to regenerate it.