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 10 minutes. You'll build a simple product management API with a React frontend.

Prerequisites

Make sure you've completed the Installation guide before starting.

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

Step 1: Create Project Structure

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

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

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: admin123
    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 / admin123
  • 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.