Jetzig

Jetzig

Description

Jetzig is a very complete and opinionated web framework. It offers all the basics of a web framework, such as routing, middleware and cookies. It also offers a templating language written in Zig. The project aims to be 100% Zig and not use any C code. It is also relatively fast because it uses http.zig as its HTTP server.

CLI

Jetzig project structure are quite complex and opinionated, that is why it comes with a CLI to generate a new project.

To install the CLI I recommend directly cloning the repository instead of downloading the zip file so that you avoid downloading an outdated version of the CLI, indeed updates are frequent as they hve to keep up with Zig and http.zig.

  git clone https://github.com/jetzig-framework/jetzig
  cd jetzig/cli
  zig build install
  mv zig-out/bin/jetzig /usr/local/bin // Assuming /usr/local/bin is in your PATH

And after that you should be able to use the jetzig CLI anywhere in your terminal.

You can use this command to see all the available commands:

  jetzig --help

The CLI can do far more than simply generate a new project, it can generate all the different components of a Jetzig project, such as views, layouts and middlewares.

Project structure

The framework being very opinionated, it forces the user to comply to a strict project structure that looks like this.

    • favicon.ico
    • styles.css
    • jetzig.png
                • index.zmpl
                • index.zmpl
                • get.zmpl
                • put.zmpl
                • post.zmpl
                • patch.zmpl
                • delete.zmpl
              • root.zig
              • viewo.zig
          • main.zig
        • build.zig
        • build.zig.zon
        • For each view you create, you get a zig file for handling all the different available HTTP methods and for each you get a zmpl file in a folder with the same name as the view.

          Basic example

          Let's take back the structure we just mentionned and look at the viewo.zig endpoint. Your entry file looks like this.

            const std = @import("std");
            const jetzig = @import("jetzig");
            
            pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
                _ = data;
                return request.render(.ok);
            }
            
            pub fn get(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
                _ = data;
                _ = id;
                return request.render(.ok);
            }
            
            pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
                _ = data;
                return request.render(.created);
            }
            
            pub fn put(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
                _ = data;
                _ = id;
                return request.render(.ok);
            }
            
            pub fn patch(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
                _ = data;
                _ = id;
                return request.render(.ok);
            }
            
            pub fn delete(id: []const u8, request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
                _ = data;
                _ = id;
                return request.render(.ok);
            }

          In this endpoint you can pass data to your zmpl file. Like here where we pass the name parameter of the request or the default "Jeremie" string.

            pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
                var object = try data.root(.object);
            
                const value = try request.params();
            
                if (value.get("name")) |name| {
                    try object.put("name", name);
                } else {
                    try object.put("name", data.string("Jeremie"));
                }
            
                return request.render(.ok);
            }

          If you visit http://localhost:8080/viewo you will get a WEB page containing the following.

            Your name is Jeremie

          And if you visit http://localhost:8080/viewo?name=John you will get the personalised input.

            Your name is John
          Cookies

          You can do exactly the same but with cookies.

          pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
              var object = try data.root(.object);
          
              var cookies = try request.cookies();
              try cookies.put(.{ .name = "username", .value = "Spongebob" });
          
              if (cookies.get("username")) |name| {
                  try object.put("name", name.value);
              } else {
                  try object.put("name", data.string("unknown"));
              }
          
              return request.render(.ok);
          }
          Tailwind Middleware

          One of the upcoming features of Jetzig is a Tailwind middleware that allow to only include the CSS rules used by your project to keep the file size as small as possible, however before this feature comes out you have two options.

          The first is to simply use the CDN of Tailwind in the .zmpl file.

            <!DOCTYPE html>
            <html lang="en">
            
            <head>
              <meta charset="UTF-8">
              <meta name="viewport" content="width=device-width, initial-scale=1.0">
              <title>Tailwind CSS</title>
              <link href="../../../output.css" rel="stylesheet">
            </head>
            
            <body>
              <div class="h1 bg-red-500 text-white p-4 rounded">
                Hello with my CDN Tailwind!
              </div>
            </body>
            
            </html>

          The problem with this solution is that you are going to serve the entire Tailwind CSS file at runtime for each request.

          The second which makes most of the work at build time is according to the author of the framework himself it is almost equivalent to what he imagined for the Tailwind middleware. We are simply going to install Tailwind in our project following the official documentation and modifying some parts to fit the structure of our project.

            # At the root of our project folder
            npm install -D tailwindcss
            npx tailwindcss init

          The modify the tailwind.config.js file to fit the structure of our project.

            /** @type {import('tailwindcss').Config} */
            module.exports = {
              content: ["./src/app/views/**/*.zmpl"],
              theme: {
                extend: {},
              },
              plugins: [],
            }

          Then in our /public directory create an input.css file.

            @tailwind base;
            @tailwind components;
            @tailwind utilities;

          Then simply start the Tailwind build process.

            npx tailwindcss -i ./public/input.css -o ./public/output.css --watch

          To conclude with this second solution you do not really need the future Tailwind middleware.

          HTMX Middleware

          The HTMX middleware is going to allow only partial pages reloads, without calling the endpoint agains and do a full page refresh when calling hx-get and hx-target together. More informations can be found on this discord answer from the author itself.

          To leverage the middleware just uncomment this line in the main.zig file.

            jetzig.middleware.HtmxMiddleware

          Other than that you can just use HTMX as you would normally do.

          Conclusion

          Jetzig is a very complete and begginer friendly web framework, the CLI guides by creating whatever you need and puting it in the right place, making all the Jetzig projects follow the same structure. The framework is currently under heavy development and plan to add even more features like database integration into the project. The templating language allows you to develop your frontend and backend in the same language, which is a great advantage for small projects. The only downside is that there are a lot of features and not all of them are deeply documented yet, note that all the the generated code is heavily commented so you can try to understand what is happening by reading the source code. Having such an opiniated framework can also be annyoing sometimes, for example you have to have 1 file per endpoint plus a whole folder containing all the different possible views for each endpoint, this can be problematic if you write a lot of small endpoints, having to have at least 2 files and 1 folder for each of them can be a bit annoying.