# Go Web Starter Kit This is a fairly complete web development starter kit in Go. It tries to be as simple as possible without leaving out modern features. The goal is to create a setup that a normal person can learn and study core web technologies like the DOM and web servers. I would classify it as, "Old school with modern things." ## The Stack Currently I'm using the following components in a tastefully crafted stack similar to a Crepe Cake: * [goose](https://github.com/pressly/goose) -- This manages the database migrations. * [sqlx](https://github.com/jmoiron/sqlx) -- This is the database driver. * [squirrel](https://github.com/Masterminds/squirrel) -- This is the SQL query generator. * [Fiber](https://gofiber.io/) -- This is the main web API and does most everything you need. * [tailwind](https://tailwindcss.com/) -- This makes your sites perty and is easy to use, but I reject the `@apply` slander and use `@apply` to give you nice things. * [ssgod](https://lcthw.dev/go/ssgod) -- A static site generator I wrote that _only_ does static site generation and nothing else...unlike Hugo. * [ozai](https://lcthw.dev/go/ozai) -- A process manager that replaced Air because Air went crazy like Hugo. * [chromedp](https://github.com/chromedp/chromedp) -- This does your automated testing for you. I then add a few little "sweetener" APIs on top of this for simple things like, making the tests nicer or making it easier to quickly return a page from a view. ## Getting Started First, install a couple tools that'll be used everywhere, and that have huge dependencies you don't actually need in your project: ```shell go install golang.org/x/pkgsite/cmd/pkgsite@latest go install github.com/pressly/goose/v3/cmd/goose@latest ``` To start your own project do this: ```shell git clone https://lcthw.dev/go/go-web-starter-kit.git my-project cd my-project ``` Then on Linux/OSX you want to delete the `.git` with this: ```shell rm -rf .git mv LICENSE LICENSE.old # it's MIT ``` And on Windows you use the ever more clear and totally easier: ```shell rm -recurse -force .git mv LICENSE LICENSE.old # it's MIT ``` Once you have that you can make it your own: ```shell git init . go mod tidy cp config_example.json config.json make migrate_up sqlite3 db.sqlite3 ".read tools/pragmas.sql" make make dev ``` This gets the site running and ready for development. You only need _to do this once._. The next day that you want to work, cd to your project and only run `make dev`: ```shell make dev ``` ## Linux Bug? I found that on Ubuntu the `tailwindcss` command "wouldn't run." Turns out when `tailwindcss` forks it does the right thing and closes all possible open file descriptors, _but_ Ubuntu has set the hard limit to 1 billion: ```shell $ ulimit -Hn 1073741816 ``` This is a _lot_ of files to try to close, so `tailwindcss` is actually stuck doing that. You can fix this with: ```shell ulimit -n 65536 ``` You can pick any reasonable number, and then `tailwindcss` works like expected. ## Configuration There's 3 basic moving parts to the whole system: `webapp`, `ssgod`, `tailwind`, and `ozai`. You can configure the `webapp` using the `config.json` which you copied over from `config_example.json`. The options are fairly self-explanatory. You can configure [ssgod](https://lcthw.dev/go/ssgod) using the `ssgod.json` file. You'll notice that there's _two_ layouts with this setup, one for `webapp` and another for `ssgod`. You can just point `ssgod` at your `views/layouts/main.html` file if you want, but it seems people want two different layouts for their static site vs. "the app." You can configure `tailwind` using the `static/input_style.css` and editing the `Makefile` to have it run how you want. I use the `@apply` heavily because I'm a programmer and no programmer would ever tell someone they should sprinkle repetitive bullshit all over their HTML. Resist the authority. Use `@apply`. Finally, you can configure other processes to run with [Ozai](https://lcthw.dev/go/ozai) in the `.ozai.json` file. Why so many different formats? I'm still learning which is best to use and while I like TOML I think I may go with `.json` files to reduce dependencies. You tell me though. ## Ozai Autorestarts Ozai is a simpler tool that doesn't do auto restart. Instead it will create a little webserver at `http://127.0.0.1:9999` with endpoints you can hit with curl to force a restart. Look in the .ozai.json file to see what's configured. This makes it easy to simple add a `curl` call in your build process and kick everything over. Look in the `Makefile` for how I do this. This works better than the way `Air` does it for a few reasons: 1. You can run your build like normal in your own IDE or build tool and see your errors directly. When you use other autobuild tools they have the errors so you can't access them. 2. You get an immediate restart after your build succeeds, rather than waiting for an external tool to detect you made changes, run the build, and then restart your browser. 3. You avoid the "auto reload deadlock" where you run a build, the server is shutdown, but you hit refresh too soon, so now your browser waits for an error, then you have to refresh again. Way easier to just not use any of that. 4. No more silent failures hiding inside your terminal that you can't see. Just manually restart it. For more, checkout the [Ozai Project](https://lcthw.dev/go/ozai"). ## Tour of Directories The directories are organized in a way that separates "App Stuff" from "Content Stuff". * `admin` App The code for the built-in database admin tool. * `bin` App Binaries that run your app like `webapp`. * `features` App Where you put your features. * `common`App Common helper functions. * `config` App The configuration for your site, which is `config.json` loaded into Go structs. * `data` App Your database stuff. You put your SQL models in here, and then add them to the bottom `Map` to make them magically show up in the `/admin/table/` tool. * `migrations` App When you use [goose](https://github.com/pressly/goose) it puts the migrations in here. * `tests` App Your tests go in here. I use [chromedp](https://github.com/chromedp/chromedp) but the `chromedp` API is too low level (and weird) to be useful so look in `tests/tools.go` for what I use. * `tools` App Various admin automation tools go here. * `views` Content This is where you put the _App_ contents, not the static content. Sadly, this should be in `features/` but Fiber makes that too difficult. * `emails` Content This where you put templates for emails you'll send. * `pages` Content These are templates that are generated once and then served statically. * `static` Static This is static content like `.js`, images, and `.css`. The following directories are considered "build junk" and you _should not_ commit them to your git. * `public` Junk This is generated by `ssgod` and then served by your `webapp`. Don't edit anything in here, and instead edit in `pages` or `static`. * `tmp` Junk This is made during the build/testing process. ## Dev Workflow The first thing I do is run each of these commands in different terminals, either with tmux or just using my fingers to run multiple tabs in PowerShell: ```shell make dev ``` ### 1. Create a Feature The first step is to create a `feature`, which is nothing more than some files in a directory that you edit to make your `pages/` actually work. You can create a feature like this: ```shell ./bin/fgen -name myfeatue ``` This will create the files and directories you need to make a new feature: `features/myfeature/` : This contains all of the go code. `views/myfeature/` : This is where your HTML goes. `tests/myfeature/` : This is where your tests go. Next you add it to `features/init.go` to enable it: ```diff @@ -8,2 +8,3 @@ import ( "MY/webapp/features/fakepay" + "MY/webapp/features/myfeature" ) @@ -15,2 +16,3 @@ func Setup(app *fiber.App) { features_fakepay.Setup(app) + features_myfeature.Setup(app) } ``` > __NOTE__: I'm going to automate this in the future, so expect this file to be generated. After that run `make` to build everything again and you should have a new feature at `http://127.0.0.1:7001/myfeature/` with your browser. ### 2. Create Initial Views The design of this system is that you do _not_ have to edit 16 files just get an idea going. You can have an idea and create a single `.html` or `.md` file in `views/myfeature` to see it happen. This lets you work out all of the URLs, page flows, layouts, and basic design before writing any more code. This is generally a better way to work than creating the backend models first as it constrains what you create to only exactly what you need for the UI you'll implement. What I do is make a new file in `myfeature/views/`, cook up a quick 2D layout that's mostly what I want, then make it link to other `myfeature/views/` to figure out the feature. Once I have a rough UI for the feature worked out I move on to the next step. ### Using the Tailwind Starter I've included [tailwind](https://tailwindcss.com/) in this setup, since it's decent at getting things working quickly right in the HTML. The only thing I've fully rejected is the idea that "@apply is bad m'kay." No, `@apply` makes it so I don't rage at all the idiots who think 92 repeated CSS classes on every `div` is "good practice." > __NOTE__ You can view a sample page with all of these in [/examples/sample.html](/examples/sample.html) and you can look at the other files in `examples` to get an idea of what you can do. Some of these just use tailwind, others use my starter kit. In my setup there's a `static/input_style.css` that is used to generate a `static/style.css` and it comes preconfigured with many of the things you'll need to get a page up and running. In theory you can prototype a first page with just HTML and a bit of tailwind as needed. It also uses many of the stock HTML tags like `aside` and `mark` so you can keep things nice. I've added 4 additional layout tags for you to use: * `grid` -- For stuff in a grid. Add `class="grid-cols-2"` from tailwind to change its layout. * `block` -- A simple pre-configured vertical flex box. Put it around vertical stuff. * `bar` -- A simple pre-configured horizontal flex box. Pit it around horizontal stuff. * `stack` -- A simple way to stack stuff on top of other stuff. You know, like in every single 2d compositional system since the 1500s. * `shape` -- A simple 2d box to use for placeholders where you'll put images. It's a lot quicker to drop a little `shape` than to go find an image. I think it also makes it easier to focus on the design. ### 3. Create Fake Data Create a fake set of data that comes from your page prototype. For example, if I have a User Profile page then I might add this: ```html ``` This will give you something to work with while you sort out how the interactive parts of your page will work. If you don't have any then you can skip this part. ### 4. Refine the UI Interactions Using the fake data determine the user interactions for the view. Remember that this system is designed to minimize the amount of work you need on the front-end, so all you're doing here is simple things like validating a form, getting data, disabling/enabling fields, and anything that stays _on one page_. If you find yourself trying to cram multiple pages into one then you've gone too far. With my simple use-profile example I could create the function that renders the table on page load: ```javascript $boot(async() => { const tmpl = $id('table-row'); const target = $id('table-items'); for(let key in data) { $append(target, $render(tmpl, {key, value: data[key]})); } }); ``` This comes from my `jzed.js` file that's a minimalist old-school DOM helper. I also include [lit.js](https://lit.dev/) if you prefer to create little components for things. ### 5. Move the Fake Data to `api.go` You should be spending a lot of time working out the data and interactions in the nice convenience of the page you're working on, but eventually that data needs to come from the server. To continue the _User Profile_ example we can create a handler in `api.go` that returns this same fake data: ```go package features_myfeature import ( "github.com/gofiber/fiber/v2" ) type UserProfile struct { UserName string Email string } func GetApiUserProfile(c *fiber.Ctx) error { profile := UserProfile{ UserName: "Zed", Email: "zed@zed.com", } return c.JSON(profile) } func SetupApi(app *fiber.App) { app.Get("/api/myfeature/user_profile", GetApiUserProfile) } ``` This is just enough code to get a simple JSON API going that we can load from the `index.html` you've been editing. To grab the data just do this: ```javascript const [data, status] = await $get_json('/api/myfeature/user_profile'); if($no_error(data, status)) { // use it } ``` In place of your prototype data. Doing that will get the data from the `api.go` code you just wrote. ### 6. Store the Data in `db.go` Once you've sorted out how the data is going to come from your API you can start to store it in the database. The first step for that is to move your data into `db.go`: ```go package features_myfeature type UserProfile struct { UserName string Email string } func GetUserProfile(id int) (UserProfile, error) { profile := UserProfile{ UserName: "John", Email: "zed@zed.com", } return profile, nil } ``` The `UserProfile` is removed from `api.go` and placed in `db.go`. You then call your `GetUserProfile` function to get it: ```go import ( "github.com/gofiber/fiber/v2" . "MY/webapp/common" ) func GetApiUserProfile(c *fiber.Ctx) error { profile, err := GetUserProfile(1) if err != nil { return IfErrNil(err, c) } return c.JSON(profile) } ``` > __NOTE__: This is intended as a simplified way to _grow_ a feature from idea to working backend. > Eventually you would want to create a separate module for your data models, create your tests for > them, and other important design considerations. ### 7. Move Hard Stuff to `views.go` There's some things that are too hard to do in JavaScript, but are very easy to do in `views.go` usign the basic templates available. An example of this is defining the structure of a `