Skip to main content

Quick Start - Build Your First App

Developer Beginner

This guide will walk you through creating your first EZ-Console application in about 10 minutes. You'll build a simple product management API with a React frontend.

Prerequisites

Before you start, install the following on your machine.

Required software

1. Go (1.20 or higher)

Check:

go version
# Should output: go version go1.20.x or higher

Install: golang.org/dl (Windows/macOS installers; Linux tarball or distro packages). On macOS you can use brew install go.

Optional — GOPATH on older setups:

export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin

2. Node.js (18 or higher)

Check:

node --version
# Should output: v18.x.x or higher

Install: nodejs.org or your OS package manager / NodeSource on Linux.

3. pnpm

Check:

pnpm --version

Install:

npm install -g pnpm

Other install options: pnpm.io/installation.

4. Git

Check: git --version

Install: git-scm.com or your OS package manager (brew install git, apt install git, etc.).

  • Make — for generated project Makefile workflows (make --version; on Linux, build-essential often includes it).
  • Docker — for deployment-style setups (docker.com).

Encryption key (when you run the server)

EZ-Console requires --global.encrypt-key (or equivalent config / env) to encrypt sensitive data in the database.

Key length and rotation

The value for --global.encrypt-key must be 8, 16, 24, or 32 bytes (character length for ASCII). This key encrypts sensitive data at rest. Do not change it after initial production use or existing encrypted values can become unreadable.

Examples used in this guide (development only):

go run main.go --global.encrypt-key=test-key-16bytes

For flag precedence and related options, see CLI commands.

What We'll Build

We're going to create a simple product management system with:

  • Backend: A RESTful API for managing products
  • Frontend: A React page to display and interact with products
  • Full Integration: Frontend calling backend APIs

The framework repository ships a Cobra CLI that can scaffold a minimal backend + web tree (root Makefile, backend/, web/ with Vite, React, and the ez-console npm package). Build or install the ez-console binary from github.com/sven-victor/ez-console, then run init from a shell:

ez-console init product-manager \
--name product-manager \
--go-module github.com/yourusername/product-manager/backend
cd product-manager
Argument / flagMeaning
product-managerProject root directory (created if missing)
--nameService name passed to NewCommandServer; also used for the default SQLite file name in generated backend/config.yaml
--go-moduleModule path for backend/go.mod (must match what you use in Go imports)

Optional flags include --app-version (default 0.1.0), --ez-console-version (pinned github.com/sven-victor/ez-console module version), and --description.

Typical follow-up: from the project root, make build builds web + backend; run the backend from backend/ (go run main.go with your encrypt key), and the web dev server from web/ (pnpm install then pnpm run start — the generated Vite config proxies /api and /console to http://localhost:8080). To develop against a local clone of the framework, add a replace github.com/sven-victor/ez-console => /path/to/ez-console in backend/go.mod and run go mod tidy.

If you use init for this tutorial: skip Step 1 below. In Step 2, keep your module path consistent with --go-module instead of github.com/yourusername/product-manager. In Step 3, merge or add the files shown into the generated web/ tree (pnpm install first) instead of creating an empty web/ from scratch.

Full flag list and generate stubs: CLI commands (aligned with the upstream usages/18-code-generation-cli.md guide in the framework repo).

Step 1: Create Project Structure

Skip if you used ez-console init

If you already scaffolded with ez-console init, you have backend/ and web/ — go to Step 2.

Create your project directory:

mkdir product-manager
cd product-manager
mkdir -p backend/controller backend/service
mkdir -p web/src/pages

Your initial structure:

product-manager/
├── backend/
│ ├── controller/
│ └── service/
└── web/
└── src/
└── pages/

Step 2: Initialize Backend

If you used ez-console init

You already have backend/go.mod, backend/main.go, and sample packages. Add backend/controller/product.go as below (or alongside the samples). Ensure main.go blank-imports your controller package if it does not already. Use the same module path as --go-module. Skip subsections that duplicate what the scaffold created (go mod init, base main.go).

Create Go Module

cd backend
go mod init github.com/yourusername/product-manager

Install Dependencies

go get github.com/gin-gonic/gin
go get github.com/sven-victor/ez-console@latest

Create Main Entry Point

Create backend/main.go:

package main

import (
_ "github.com/yourusername/product-manager/controller"

consoleserver "github.com/sven-victor/ez-console/server"
)

const VERSION = "1.0.0"

var rootCmd = consoleserver.NewCommandServer(
"product-manager",
VERSION,
"Product Management System",
)

func main() {
rootCmd.Execute()
}

Create Product Controller

Create backend/controller/product.go:

package controller

import (
"context"
"net/http"
"time"

"github.com/gin-gonic/gin"
"github.com/sven-victor/ez-console/server"
"github.com/sven-victor/ez-console/pkg/util"
)

// Product represents a product
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
Stock int `json:"stock"`
CreatedAt time.Time `json:"created_at"`
}

// ProductController handles product endpoints
type ProductController struct {
svc server.Service
}

// In-memory storage (for demo purposes)
var products = []Product{
{
ID: "1",
Name: "Laptop",
Description: "High-performance laptop",
Price: 1299.99,
Stock: 50,
CreatedAt: time.Now(),
},
{
ID: "2",
Name: "Mouse",
Description: "Wireless mouse",
Price: 29.99,
Stock: 200,
CreatedAt: time.Now(),
},
{
ID: "3",
Name: "Keyboard",
Description: "Mechanical keyboard",
Price: 99.99,
Stock: 100,
CreatedAt: time.Now(),
},
}

// ListProducts returns all products
func (c *ProductController) ListProducts(ctx *gin.Context) {
util.RespondWithSuccess(ctx, http.StatusOK, products)
}

// GetProduct returns a single product
func (c *ProductController) GetProduct(ctx *gin.Context) {
id := ctx.Param("id")

for _, product := range products {
if product.ID == id {
util.RespondWithSuccess(ctx, http.StatusOK, product)
return
}
}

util.RespondWithError(ctx, util.NewErrorMessage("E4041", "Product not found"))
}

// CreateProduct creates a new product
func (c *ProductController) CreateProduct(ctx *gin.Context) {
var req Product

if err := ctx.ShouldBindJSON(&req); err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Invalid request", err))
return
}

// Generate ID and set created time
req.ID = util.GenerateUUID()
req.CreatedAt = time.Now()

products = append(products, req)

util.RespondWithSuccess(ctx, http.StatusCreated, req)
}

// RegisterRoutes registers all routes for this controller
func (c *ProductController) RegisterRoutes(ctx context.Context, router *gin.RouterGroup) {
products := router.Group("/products")
{
products.GET("", c.ListProducts)
products.GET("/:id", c.GetProduct)
products.POST("", c.CreateProduct)
}
}

// NewProductController creates a new product controller
func NewProductController(svc server.Service) *ProductController {
return &ProductController{svc: svc}
}

// Register the controller with the framework
func init() {
server.RegisterControllers(func(ctx context.Context, svc server.Service) server.Controller {
return NewProductController(svc)
})
}

Run the Backend

go run main.go --global.encrypt-key=test-key-16bytes

The server will start on http://localhost:8080.

Test the API

# List all products
curl http://localhost:8080/api/products

# Get a single product
curl http://localhost:8080/api/products/1

# Create a product
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{
"name": "Monitor",
"description": "4K Monitor",
"price": 399.99,
"stock": 30
}'
API Response Format

All successful responses follow this format:

{
"code": "0",
"data": { /* your data here */ }
}

Step 3: Initialize Frontend

If you used ez-console init

The scaffold already includes package.json, vite.config.ts, and TypeScript config with /api (and /console) proxying to http://localhost:8080. Run pnpm install in web/, then add or merge src/App.tsx, src/pages/ProductPage.tsx, src/main.tsx, and index.html as shown below (adjust paths if the generated layout differs). You may keep the generated pnpm run start script instead of pnpm dev if that is what the template provides.

Create package.json

In a new terminal, navigate to the web directory:

cd web

Create web/package.json:

{
"name": "product-manager-web",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"ahooks": "^3.9.6",
"antd": "^5.14.1",
"ez-console": "github:sven-victor/ez-console.git#v1.9.1&path:web",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^20.11.19",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",
"less": "^4.2.2",
"typescript": "^5.3.3",
"vite": "^5.1.0"
}
}

Install Dependencies

pnpm install

Create Vite Configuration

Create web/vite.config.ts:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})

Create TypeScript Configuration

Create web/tsconfig.json:

{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

Create web/tsconfig.node.json:

{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

Create App Component

Create web/src/App.tsx:

import { ConfigProvider } from 'antd';
import { EZApp, i18n, withSuspense } from 'ez-console';
import { lazy } from 'react';

const ProductPage = lazy(() => import('@/pages/ProductPage'));

// Add translations
i18n.addResource('en', 'translation', 'menu.products', 'Products');
i18n.addResource('en', 'translation', 'products.title', 'Product Management');
i18n.addResource('en', 'translation', 'products.name', 'Name');
i18n.addResource('en', 'translation', 'products.description', 'Description');
i18n.addResource('en', 'translation', 'products.price', 'Price');
i18n.addResource('en', 'translation', 'products.stock', 'Stock');

export default function App() {
return (
<ConfigProvider
theme={{
token: {
colorPrimary: '#1890ff',
}
}}
>
<EZApp
basePath='/'
extraPrivateRoutes={[
{
path: '/products',
element: withSuspense(ProductPage),
name: 'products',
index: true,
}
]}
/>
</ConfigProvider>
);
}

Create Product Page

Create web/src/pages/ProductPage.tsx:

import { Card, Table, Tag, Typography, Space, Button, Modal, Form, Input, InputNumber, message } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { useTranslation, apiGet, apiPost } from 'ez-console';
import { useRequest } from 'ahooks';
import { useState } from 'react';

const { Title } = Typography;
const { Column } = Table;

interface Product {
id: string;
name: string;
description: string;
price: number;
stock: number;
created_at: string;
}

export const ProductPage: React.FC = () => {
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState(false);
const [form] = Form.useForm();

// Fetch products
const { data, loading, refresh } = useRequest(
() => apiGet<Product[]>('/products')
);

// Create product
const { run: createProduct, loading: creating } = useRequest(
(values) => apiPost('/products', values),
{
manual: true,
onSuccess: () => {
message.success('Product created successfully');
setIsModalOpen(false);
form.resetFields();
refresh();
},
onError: (error) => {
message.error('Failed to create product');
},
}
);

const handleCreate = () => {
form.validateFields().then((values) => {
createProduct(values);
});
};

return (
<Card>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={2}>{t('products.title')}</Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setIsModalOpen(true)}
>
Add Product
</Button>
</div>

<Table
dataSource={data}
loading={loading}
rowKey="id"
pagination={{ pageSize: 10 }}
>
<Column title={t('products.name')} dataIndex="name" key="name" />
<Column
title={t('products.description')}
dataIndex="description"
key="description"
/>
<Column
title={t('products.price')}
dataIndex="price"
key="price"
render={(price) => `$${price.toFixed(2)}`}
/>
<Column
title={t('products.stock')}
dataIndex="stock"
key="stock"
render={(stock) => (
<Tag color={stock > 50 ? 'green' : stock > 0 ? 'orange' : 'red'}>
{stock} units
</Tag>
)}
/>
</Table>
</Space>

<Modal
title="Create Product"
open={isModalOpen}
onOk={handleCreate}
onCancel={() => setIsModalOpen(false)}
confirmLoading={creating}
>
<Form form={form} layout="vertical">
<Form.Item
name="name"
label="Product Name"
rules={[{ required: true, message: 'Please input product name' }]}
>
<Input placeholder="Enter product name" />
</Form.Item>
<Form.Item
name="description"
label="Description"
rules={[{ required: true, message: 'Please input description' }]}
>
<Input.TextArea rows={3} placeholder="Enter description" />
</Form.Item>
<Form.Item
name="price"
label="Price"
rules={[{ required: true, message: 'Please input price' }]}
>
<InputNumber
min={0}
precision={2}
style={{ width: '100%' }}
prefix="$"
/>
</Form.Item>
<Form.Item
name="stock"
label="Stock"
rules={[{ required: true, message: 'Please input stock' }]}
>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
</Card>
);
};

export default ProductPage;

Create Entry Point

Create web/src/main.tsx:

import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import 'ez-console/lib/style.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
<App />
);

Create HTML Template

Create web/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Product Manager</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Run the Frontend

pnpm dev

The frontend will start on http://localhost:5173.

Step 4: Test Your Application

  1. Open your browser to http://localhost:5173

  2. Login with default credentials:

    • Username: admin
    • Password: Admin@123
    First Login

    You'll be prompted to change the password on first login.

  3. Navigate to the Products page from the menu

  4. View products - You should see the 3 default products

  5. Create a product - Click "Add Product" and fill in the form

  6. Verify the product appears in the list

Project Structure

Your complete project should look like this:

product-manager/
├── backend/
│ ├── controller/
│ │ └── product.go
│ ├── main.go
│ ├── go.mod
│ └── go.sum
└── web/
├── src/
│ ├── pages/
│ │ └── ProductPage.tsx
│ ├── App.tsx
│ └── main.tsx
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

What You've Learned

Congratulations! You've just built a full-stack application with EZ-Console. You've learned:

Backend

  • ✅ How to create a Go module
  • ✅ How to create controllers
  • ✅ How to define API endpoints
  • ✅ How to handle requests and responses
  • ✅ How to use the framework's utility functions

Frontend

  • ✅ How to set up a Vite project
  • ✅ How to use the EZApp shell
  • ✅ How to create pages
  • ✅ How to call backend APIs
  • ✅ How to use Ant Design components

Integration

  • ✅ How frontend and backend work together
  • ✅ How to handle authentication
  • ✅ How to proxy API requests in development

Next Steps

Now that you've built your first application, here's what to explore next:

Improve the Backend

  1. Add a Service Layer - Move business logic from controller to service
  2. Use a Database - Replace in-memory storage with GORM
  3. Add Validation - Implement request validation
  4. Add Authentication - Protect endpoints with JWT
  5. Add Pagination - Implement list pagination

Learn more in Backend Development.

Enhance the Frontend

  1. Add Edit/Delete - Implement CRUD operations
  2. Add Filters - Implement search and filtering
  3. Add Pagination - Handle large datasets
  4. Improve UI - Add more components
  5. Add Forms - Create complex forms with validation

Learn more in Frontend Development.

Production Ready

  1. Error Handling - Implement proper error handling
  2. Logging - Add structured logging
  3. Testing - Write unit and integration tests
  4. Configuration - Use config files and environment variables
  5. Deployment - Deploy to production

Learn more in Deployment.

Operate via the admin console

  1. Sign in at /console and change the default admin password
  2. Users & rolesUser Management, Role & Permission Management
  3. MonitoringAudit Log Viewer, Statistics & Reports

Troubleshooting

Backend won't start

  • Verify Go is installed: go version
  • Check if port 8080 is available
  • Ensure encryption key is correct length

Frontend won't build

  • Delete node_modules and reinstall: rm -rf node_modules && pnpm install
  • Verify Node.js version: node --version
  • Check for TypeScript errors

API calls fail

  • Ensure backend is running on port 8080
  • Check Vite proxy configuration
  • Verify API paths are correct

Can't login

  • Use default credentials: admin / Admin@123
  • Check browser console for errors
  • Verify JWT token is being sent

Admin console

Additional Resources

  • Full Example: Check the /demo directory in the repository
  • API Documentation: http://localhost:8080/swagger
  • Component Library: Ant Design 5 documentation
  • React Hooks: ahooks documentation

Built your first app? Great! Continue to Core Concepts to deepen your understanding, or jump to Backend Development to build more complex APIs.