A comprehensive tutorial on building a Parka server from scratch, covering basic setup to advanced features
This tutorial will guide you through building a Parka server from scratch, covering everything from basic setup to advanced features. We'll create a complete application that demonstrates the various capabilities of Parka.
Before diving into the implementation, let's understand how Parka works and how its components fit together.
Parka is built around several key concepts:
┌─────────────────────────────────────────────────┐
│ Parka Server │
├─────────────┬─────────────────┬────────────────┤
│ Static │ Template │ Command │
│ Handlers │ Handlers │ Handlers │
├─────────────┴─────────────────┴────────────────┤
│ Echo Framework │
├──────────────────────────────────────────────┬─┤
│ Command Repository │M││
├──────────────────────────────────────────────┤i││
│ Commands │d││
├──────────────────────────────────────────────┤d││
│ Glazed Framework │w││
└──────────────────────────────────────────────┴─┘
Request Flow:
Handler Types:
Command Integration:
Configuration:
Now that we understand the big picture, let's build our application step by step.
First, let's create a new Go project. This structure will help us organize our code according to Parka's architecture:
mkdir my-parka-app
cd my-parka-app
go mod init my-parka-app
Add the required dependencies:
go get github.com/go-go-golems/parka # Core Parka framework
go get github.com/labstack/echo/v4 # Web framework
go get github.com/spf13/cobra # CLI framework
Let's start with a basic server structure. This forms the foundation of our application:
package main
import (
"context"
"github.com/go-go-golems/parka/pkg/server"
"github.com/spf13/cobra"
"os"
"os/signal"
)
var rootCmd = &cobra.Command{
Use: "my-server",
Short: "A Parka-based web server",
Run: func(cmd *cobra.Command, args []string) {
port, _ := cmd.Flags().GetUint16("port")
host, _ := cmd.Flags().GetString("host")
s, err := server.NewServer(
server.WithPort(port),
server.WithAddress(host),
server.WithGzip(),
)
cobra.CheckErr(err)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
defer stop()
err = s.Run(ctx)
cobra.CheckErr(err)
},
}
func init() {
rootCmd.Flags().Uint16("port", 8080, "Port to listen on")
rootCmd.Flags().String("host", "localhost", "Host to listen on")
}
func main() {
_ = rootCmd.Execute()
}
Command-Line Interface:
Server Configuration:
Signal Handling:
mkdir -p static/css static/js
Static file serving is essential for web applications. Here's how Parka handles it:
File System Abstraction:
fs.FS interfaceHandler Configuration:
Let's add static file serving for CSS, JavaScript, and other assets:
// In your Run function
staticHandler := static_dir.NewStaticDirHandler(
static_dir.WithLocalPath("./static"),
)
err = staticHandler.Serve(s, "/assets")
if err != nil {
return err
}
Templates are crucial for generating dynamic HTML content. Parka's template system provides:
Create a templates directory for your HTML templates:
mkdir -p templates/layouts templates/pages
Create a base layout (templates/layouts/base.tmpl.html):
<!DOCTYPE html>
<html>
<head>
<title>{{ .Title }}</title>
<link rel="stylesheet" href="/assets/css/style.css">
</head>
<body>
<header>
<h1>{{ .Title }}</h1>
</header>
<main>
{{ template "content" . }}
</main>
<footer>
<p>© 2024 My Parka App</p>
</footer>
</body>
</html>
Layout System:
Development Support:
templateHandler := template_dir.NewTemplateDirHandler(
template_dir.WithLocalDirectory("./templates"),
template_dir.WithAlwaysReload(true), // For development
)
err = templateHandler.Serve(s, "/")
if err != nil {
return err
}
Commands are the core feature of Parka, allowing you to expose CLI tools as web services.
Command Definition:
Integration Points:
Let's create a simple command that we can expose via the web interface. Create pkg/commands/hello.go:
package commands
import (
"context"
"github.com/go-go-golems/glazed/pkg/cmds"
"github.com/go-go-golems/glazed/pkg/cmds/parameters"
"github.com/go-go-golems/glazed/pkg/types"
)
type HelloCommand struct {
cmds.BaseCommand
name string
}
func NewHelloCommand() *HelloCommand {
return &HelloCommand{
BaseCommand: cmds.BaseCommand{
Name: "hello",
Short: "A friendly greeting",
Parameters: parameters.ParameterDefinitions{
{
Name: "name",
Type: types.String,
Help: "Name to greet",
Default: "World",
Required: false,
},
},
},
}
}
func (c *HelloCommand) Run(ctx context.Context, gp cmds.GlazeProcessor) error {
return gp.AddRow(ctx, types.NewRowFromMap(map[string]interface{}{
"message": fmt.Sprintf("Hello, %s!", c.name),
}))
}
Now let's expose our command through various endpoints:
// In your Run function
helloCmd := commands.NewHelloCommand()
// JSON API endpoint
s.Router.GET("/api/hello", json.CreateJSONQueryHandler(helloCmd))
// Interactive DataTables UI
s.Router.GET("/hello", datatables.CreateDataTablesHandler(
helloCmd,
"hello.tmpl.html",
"hello",
))
// File download endpoint
s.Router.GET("/download/hello.csv", output_file.CreateGlazedFileHandler(
helloCmd,
"hello.csv",
))
The Command Directory Handler manages multiple commands in a structured way.
Repository Management:
Output Formats:
repo := repositories.NewRepository()
repo.AddCommand(commands.NewHelloCommand())
// Add more commands...
cmdHandler, err := command_dir.NewCommandDirHandler(
command_dir.WithRepository(repo),
command_dir.WithDevMode(true),
command_dir.WithGenericCommandHandlerOptions(
generic_command.WithIndexTemplateName("commands/index.tmpl.html"),
generic_command.WithTemplateName("commands/view.tmpl.html"),
),
)
cobra.CheckErr(err)
err = cmdHandler.Serve(s, "/commands")
if err != nil {
return err
}
A flexible configuration system is essential for managing complex applications.
Server Settings:
Route Configuration:
Create a configuration file structure (config/config.yaml):
server:
port: 8080
host: localhost
routes:
- path: /static
staticDir:
localPath: ./static
- path: /
templateDir:
localDirectory: ./templates
indexTemplateName: index.tmpl.html
- path: /commands
commandDirectory:
includeDefaultRepositories: true
repositories:
- ./pkg/commands
templateLookup:
directories:
- ./templates/commands
indexTemplateName: commands/index.tmpl.html
defaults:
flags:
limit: 100
Add configuration loading to your server:
type Config struct {
Server struct {
Port uint16 `yaml:"port"`
Host string `yaml:"host"`
} `yaml:"server"`
Routes []config.Route `yaml:"routes"`
}
func loadConfig(file string) (*Config, error) {
data, err := os.ReadFile(file)
if err != nil {
return nil, err
}
var cfg Config
err = yaml.Unmarshal(data, &cfg)
return &cfg, err
}
// In your Run function
cfg, err := loadConfig("config/config.yaml")
cobra.CheckErr(err)
s, err := server.NewServer(
server.WithPort(cfg.Server.Port),
server.WithAddress(cfg.Server.Host),
)
cobra.CheckErr(err)
for _, route := range cfg.Routes {
handler, err := route.CreateHandler()
cobra.CheckErr(err)
err = handler.Serve(s, route.Path)
cobra.CheckErr(err)
}
Development mode enhances the development experience with useful features.
Hot Reloading:
Debugging:
func init() {
rootCmd.Flags().Bool("dev", false, "Enable development mode")
}
// In your Run function
dev, _ := cmd.Flags().GetBool("dev")
if dev {
// Enable template reloading
templateOptions = append(templateOptions,
render.WithAlwaysReload(true),
)
// Use local assets
serverOptions = append(serverOptions,
server.WithStaticPaths(fs.NewStaticPath(os.DirFS("static"), "/assets")),
)
// Enable detailed logging
log.Logger = log.Logger.Level(zerolog.DebugLevel)
}
Proper error handling and logging are crucial for maintaining and debugging applications.
// Create a custom error handler
s.Router.HTTPErrorHandler = func(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
}
log.Error().
Err(err).
Str("path", c.Path()).
Int("status", code).
Msg("Request error")
if dev {
// Show detailed error in development
err = c.JSON(code, map[string]interface{}{
"error": err.Error(),
"stack": fmt.Sprintf("%+v", err),
})
} else {
// Show generic error in production
err = c.JSON(code, map[string]interface{}{
"error": http.StatusText(code),
})
}
if err != nil {
log.Error().Err(err).Msg("Error sending error response")
}
}
Security is a critical aspect of any web application.
Protection Layers:
Monitoring:
// Add security middleware
s.Router.Use(middleware.Secure())
s.Router.Use(middleware.CORS())
s.Router.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
// Add request ID tracking
s.Router.Use(middleware.RequestID())
// Add recovery middleware
s.Router.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
StackSize: 1 << 10, // 1 KB
LogLevel: log.ERROR,
LogErrorFunc: func(c echo.Context, err error, stack []byte) error {
log.Error().
Err(err).
Str("stack", string(stack)).
Msg("Panic recovered")
return nil
},
}))
Your final project structure should look like this:
my-parka-app/
├── cmd/
│ └── server/
│ └── main.go
├── config/
│ └── config.yaml
├── pkg/
│ ├── commands/
│ │ └── hello.go
│ └── handlers/
│ └── custom_handlers.go
├── static/
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── main.js
├── templates/
│ ├── layouts/
│ │ └── base.tmpl.html
│ ├── pages/
│ │ └── index.tmpl.html
│ └── commands/
│ ├── index.tmpl.html
│ └── view.tmpl.html
├── go.mod
└── go.sum
Start the server in development mode:
go run cmd/server/main.go --dev
For production:
go build -o my-server cmd/server/main.go
./my-server
Common issues and solutions:
Template not found
Static files not serving
Command not found
Now that we understand each component, here's how they work together in a typical request:
HTTP Request Arrives:
GET /commands/hello?name=World
Request Processing:
Response Generation:
This flow combines all the components we've built into a cohesive system.