Membuat Aplikasi Keuangan dengan Go dan JWT

Halo, Sobat Koding!
Pernah kepikiran bikin aplikasi pencatat keuangan sendiri? Bukan cuma sekadar catatan manual, tapi aplikasi yang punya sistem login, autentikasi aman, dan bisa dipakai lintas platform? Di artikel ini, kita akan membangun sebuah Aplikasi Keuangan berbasis REST API menggunakan Go sebagai backend dan JWT (JSON Web Token) untuk sistem autentikasinya. Backend yang kita buat ini tidak hanya bisa dipakai untuk web, tapi juga bisa digunakan untuk aplikasi Android, iOS, atau bahkan desktop di masa depan.
Sebagai pelengkap, kita juga akan membuat frontend sederhana menggunakan HTML, CSS, dan JavaScript murni untuk mengakses API yang sudah kita bangun. Proyek ini bukan sekadar tutorial biasa. Ini adalah proyek fullstack yang bisa kamu jadikan portfolio menunjukkan bahwa kamu memahami:
- Perancangan database
- Pembuatan REST API
- Implementasi autentikasi JWT
- Integrasi frontend dan backend
- Struktur proyek yang rapi dan scalable
Kita akan membangun semuanya dari nol sampai aplikasi siap dijalankan secara lokal. Siapkan editor favoritmu, secangkir kopi, dan mari mulai membangun sistem keuangan kita sendiri.
Gambaran Proyek
Sebelum mulai menulis kode, kita perlu memahami dengan jelas sistem apa yang akan kita bangun.
Pada proyek ini, kita akan membuat sebuah Aplikasi Keuangan berbasis web yang memiliki arsitektur terpisah antara frontend dan backend. Backend akan dibangun sebagai REST API menggunakan Go, sedangkan frontend akan menggunakan HTML, CSS, dan JavaScript untuk berkomunikasi dengan API tersebut melalui HTTP request.
Aplikasi ini memungkinkan setiap pengguna memiliki akun pribadi, sehingga data keuangan antar user tidak akan tercampur.
1. Fitur Utama Aplikasi
Berikut adalah fitur yang akan kita bangun:
- Sistem Autentikasi
- Register user baru
- Login dengan verifikasi password terenkripsi
- Menghasilkan JWT token setelah login berhasil
- Proteksi endpoint agar hanya user terautentikasi yang bisa mengakses data
- Dashboard Keuangan
- Menampilkan total saldo saat ini
- Ringkasan total pemasukan
- Ringkasan total pengeluaran
- Manajemen Transaksi
- Tambah transaksi (pemasukan atau pengeluaran)
- Edit transaksi
- Hapus transaksi
- Kategori transaksi
- Riwayat Transaksi
- Menampilkan daftar transaksi milik user
- Filter berdasarkan jenis transaksi
2. Bagaimana Sistem Ini Bekerja?
Secara sederhana, alurnya seperti ini:
- User melakukan register atau login melalui frontend.
- Backend memverifikasi data dan mengembalikan JWT token.
- Token tersebut disimpan di sisi client.
- Setiap request berikutnya ke API akan membawa token tersebut.
- Backend akan memvalidasi token sebelum memberikan akses data.
Dengan pendekatan ini, setiap request ke server memiliki identitas yang jelas. Data user A tidak mungkin bisa diakses oleh user B tanpa token yang valid.
3. Arsitektur yang Akan Digunakan
Proyek ini menggunakan pendekatan:
Client (Frontend)
⬇
REST API (Go Backend)
⬇
Database (MySQL / PostgreSQL)
Frontend hanya bertugas menampilkan data dan mengirim request. Seluruh logika bisnis, validasi, dan keamanan berada di backend. Pendekatan ini disebut separation of concern, dan ini adalah pola yang umum digunakan dalam aplikasi modern.
4. Kenapa Proyek Ini Penting untuk Portfolio?
Karena di dalamnya terdapat konsep yang sering muncul di dunia kerja:
- RESTful API design
- Authentication & Authorization
- Password hashing
- Middleware
- Database relational
- Fullstack integration
- Error handling
Ini bukan sekadar aplikasi pencatat keuangan. Ini simulasi sistem produksi skala kecil.

Teknologi yang Digunakan
Untuk membangun aplikasi ini, kita akan menggunakan stack yang sederhana tetapi cukup kuat untuk kebutuhan produksi skala kecil hingga menengah. Tujuan pemilihan teknologi di sini bukan hanya agar mudah dipelajari, tetapi juga agar relevan dengan kebutuhan industri saat ini.
1. Backend: Go (Golang) + Gin
Backend akan dibangun menggunakan Go sebagai bahasa utama. Go dipilih karena:
- Performa tinggi dan efisien
- Struktur kode yang sederhana dan tegas
- Cocok untuk membangun REST API
- Banyak digunakan di sistem backend modern
Untuk framework, kita menggunakan Gin. Framework ini ringan, cepat, dan memiliki routing yang jelas. Gin memudahkan kita dalam mengatur endpoint, middleware, serta pengolahan request dan response HTTP. Dengan kombinasi ini, kita bisa membangun API yang cepat sekaligus terstruktur dengan baik.
2. Database: PostgreSQL atau MySQL
Sebagai penyimpanan data, kita menggunakan database relasional. Alasannya:
- Data transaksi keuangan bersifat terstruktur
- Memerlukan relasi antar tabel (user dan transaksi)
- Konsistensi data sangat penting
Kamu bisa memilih PostgreSQL atau MySQL sesuai preferensi. Keduanya sangat stabil dan banyak digunakan dalam proyek nyata.
3. ORM: GORM
Daripada menulis query SQL secara manual di setiap fungsi, kita menggunakan GORM sebagai ORM (Object Relational Mapping).
GORM membantu kita:
- Mendefinisikan model sebagai struct Go
- Melakukan operasi CRUD tanpa menulis query panjang
- Mengelola relasi antar tabel
- Mengurangi risiko kesalahan query
Dengan ORM, kode menjadi lebih bersih dan mudah dirawat.
4. Autentikasi: JWT
Untuk sistem autentikasi, kita menggunakan JSON Web Token.
JWT memungkinkan:
- Autentikasi berbasis token
- Backend bersifat stateless
- Setiap request membawa identitas user
- Sistem lebih fleksibel untuk digunakan di berbagai platform
Library yang digunakan adalah golang-jwt/jwt yang populer dan banyak dipakai di komunitas Go.
5. Frontend: HTML5, CSS3, dan Vanilla JavaScript
Frontend akan dibuat menggunakan teknologi dasar web tanpa framework tambahan.
Tujuannya:
- Memahami cara kerja komunikasi HTTP secara langsung
- Mengontrol penuh request ke backend
- Menunjukkan pemahaman fundamental web
Pendekatan ini juga memperjelas bahwa backend yang kita bangun benar-benar terpisah dari frontend.
6. Tools Pendukung
Beberapa tools yang akan membantu proses pengembangan:
- Postman atau Insomnia untuk menguji endpoint API sebelum dihubungkan ke frontend
- VS Code sebagai editor kode
- Git untuk version control
- Browser modern untuk pengujian antarmuka
Tools ini bukan bagian dari aplikasi, tetapi penting dalam workflow pengembangan profesional.
Arsitektur Sistem
Dalam proyek ini, kita tidak hanya fokus membuat aplikasi yang berjalan, tetapi juga memastikan struktur kodenya rapi, mudah dikembangkan, dan tidak berantakan ketika fitur mulai bertambah. Untuk itu, kita akan menggunakan pendekatan Layered Architecture pada sisi backend.
Pendekatan ini memisahkan tanggung jawab setiap bagian sistem ke dalam layer yang berbeda, sehingga masing-masing layer memiliki peran yang jelas dan tidak saling tumpang tindih.
Kenapa Tidak Semua Kode Ditulis dalam Satu File?
Tanpa arsitektur yang jelas, proyek kecil memang terlihat sederhana. Namun begitu fitur bertambah, kode bisa berubah menjadi sulit dibaca, sulit diuji, dan sulit diperbaiki.
Dengan layered architecture, kita memastikan:
- Logika bisnis tidak tercampur dengan query database
- Handler hanya fokus menerima dan mengirim response
- Perubahan di database tidak merusak bagian lain
- Kode lebih mudah diuji dan dirawat
Pendekatan ini sangat umum digunakan dalam proyek backend profesional.
Struktur Layer yang Digunakan
Backend akan dibagi menjadi beberapa layer utama:
1. Handler (Controller)
Layer ini bertugas menerima request dari client.
Tugasnya meliputi:
- Membaca parameter dari request
- Validasi awal input
- Memanggil service yang sesuai
- Mengembalikan response dalam format JSON
Handler tidak boleh berisi logika bisnis yang kompleks.
2. Service
Service adalah jantung logika aplikasi.
Di sinilah kita akan:
- Menghitung total saldo
- Memproses transaksi
- Mengatur aturan bisnis
- Menentukan valid atau tidaknya suatu operasi
Service akan memanggil repository untuk mengambil atau menyimpan data, tetapi tidak berinteraksi langsung dengan HTTP.
Dengan cara ini, logika bisnis bisa diuji secara terpisah.
3. Repository
Repository bertugas berinteraksi langsung dengan database.
Tugasnya:
- Menjalankan operasi CRUD
- Mengambil data berdasarkan kondisi tertentu
- Mengelola relasi antar tabel
Repository tidak mengetahui apa itu HTTP, tidak tahu apa itu JWT. Dia hanya tahu cara berbicara dengan database.
Alur Request dalam Sistem
Agar lebih jelas, berikut gambaran alurnya:

Client mengirim request
⬇
Handler menerima request
⬇
Service memproses logika
⬇
Repository mengambil/menyimpan data
⬇
Response dikembalikan ke client
Dengan pola ini, setiap bagian sistem punya tanggung jawab yang jelas.
Struktur Folder yang Akan Digunakan
Supaya makin konkret, kita akan menggunakan struktur folder seperti berikut:
- cmd/
- internal/handler
- internal/service
- internal/repository
- internal/model
- internal/middleware
- config/
Struktur ini membuat proyek lebih modular dan scalable.
Jika suatu saat aplikasi berkembang menjadi lebih besar, struktur ini tetap bisa dipertahankan tanpa perlu refactor besar-besaran.
Desain Database
Database adalah fondasi utama dari aplikasi ini. Seluruh data user dan transaksi akan disimpan secara terstruktur agar konsisten, aman, dan mudah dikelola. Karena aplikasi ini bersifat multi-user (setiap pengguna punya data masing-masing), kita membutuhkan relasi yang jelas antara akun dan transaksi yang dimilikinya.
Untuk kebutuhan proyek ini, kita menggunakan dua tabel utama:
- users
- transactions
Meskipun terlihat sederhana, struktur ini sudah cukup untuk membangun sistem keuangan pribadi yang solid.
1. Tabel Users
Tabel ini menyimpan data akun pengguna.
Kolom yang digunakan:
- id
Primary key dengan tipe auto increment atau UUID. - username
Nama unik untuk identitas user. - email
Digunakan untuk login dan identifikasi akun. - password
Disimpan dalam bentuk hash, bukan teks asli. - created_at
Waktu pembuatan akun.
Beberapa catatan penting:
- Password tidak boleh disimpan dalam bentuk plain text.
- Email sebaiknya dibuat unique agar tidak ada duplikasi akun.
- id akan menjadi penghubung ke tabel transaksi.
2. Tabel Transactions
Tabel ini menyimpan semua aktivitas keuangan user.
Kolom yang digunakan:
- id
Primary key transaksi. - user_id
Foreign key yang mengarah ke tabel users. - amount
Nominal uang (positif). - type
Menentukan apakah transaksi income atau expense. - description
Keterangan transaksi. - date
Tanggal transaksi dilakukan.
Relasi yang digunakan adalah:
Satu user dapat memiliki banyak transaksi.
One-to-many relationship.
Artinya:
users.id → transactions.user_id
Dengan struktur ini, setiap transaksi selalu terikat pada satu user tertentu.
Relasi Antar Tabel
Secara konseptual, hubungan antar tabel bisa digambarkan seperti ini:
Users (1)
⬇
Transactions (Many)
Artinya satu user bisa memiliki banyak transaksi, tetapi satu transaksi hanya boleh dimiliki oleh satu user.
Relasi ini penting untuk menjaga isolasi data antar pengguna. Ketika user melakukan login dan mengakses dashboard, backend hanya akan mengambil transaksi berdasarkan user_id yang sesuai dengan token JWT miliknya.
Pertimbangan Desain
Beberapa keputusan desain yang kita ambil:
- Tidak membuat tabel kategori terpisah untuk menjaga kompleksitas tetap sederhana.
- Tidak menyimpan saldo secara langsung di database. Saldo akan dihitung berdasarkan total income dikurangi total expense.
- Menggunakan tipe data numerik yang presisi untuk amount agar tidak terjadi kesalahan pembulatan.
Pendekatan ini menjaga sistem tetap ringan namun tetap realistis.
Kenapa Desain Ini Sudah Cukup?
Untuk skala aplikasi keuangan pribadi, dua tabel ini sudah memadai.
Jika di masa depan ingin dikembangkan, sistem ini masih bisa diperluas dengan:
- Tabel kategori
- Tabel refresh token
- Tabel audit log
- Soft delete pada transaksi
Desain yang sederhana tetapi extensible jauh lebih baik daripada sistem yang kompleks sejak awal.

Setup Proyek
Oke, sekarang masuk ke fase yang bikin tangan gatal pengen ngetik
Saatnya bikin pondasi project-nya dulu biar nggak asal taruh file kayak lemari kosan.
Inisialisasi Project
Buka terminal, lalu eksekusi:
mkdir finance-app-go
cd finance-app-go
go mod init finance-app
Penjelasan singkat:
mkdir→ bikin folder projectgo mod init→ inisialisasi module Go (wajib biar dependency rapi)
Kalau ini berhasil, kamu resmi punya project Go yang “beradab”.
Install Dependency Utama
Sekarang kita pasang senjata tempur dulu:
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u github.com/golang-jwt/jwt/v5
go get -u golang.org/x/crypto/bcrypt
Kenapa ini penting?
- Gin → framework HTTP (cepat & minimalis)
- GORM → ORM biar nggak nulis SQL mentah terus
- Postgres driver → koneksi database
- JWT → autentikasi token
- bcrypt → hashing password
Udah siap jadi backend engineer dadakan
Struktur Folder Awal
Sekarang kita bikin struktur folder yang rapi dari awal. Jangan tunggu project jadi 2000 baris baru panik.
Strukturnya kira-kira begini:
finance-app/
├── cmd/
├── internal/
│ ├── handlers/
│ ├── models/
│ └── services/
├── main.go
└── .env
Penjelasan singkat biar nggak cuma jadi dekorasi:
- cmd/ → entry point kalau nanti mau scale (misal CLI, worker, dll)
- internal/ → semua logic inti aplikasi
- handlers/ → nerima request HTTP
- models/ → struktur database
- services/ → business logic
- main.go → starting point aplikasi
- .env → simpan config (DB URL, secret key, dll)
Bikin Folder Cepat via Terminal
Kalau mau langsung praktis:
mkdir -p cmd internal/handlers internal/models internal/services
touch main.go .env
Selesai. Project kamu sekarang nggak lagi kosong melompong
Implementasi Backend REST API (Go + JWT)
Di tahap ini kita fokus ke autentikasi dulu. Kenapa auth dulu?
Karena:
- Semua transaksi harus punya user_id
- Nggak mungkin bikin fitur keuangan tanpa tahu siapa pemilik datanya
- JWT nanti dipakai buat proteksi endpoint lain
Strateginya sederhana:
- User register
- User login
- Backend generate JWT
- Client simpan token
- Setiap request kirim token di header
Flow Endpoint Auth
POST /register
Tugasnya:
- Validasi input
- Hash password pakai bcrypt
- Simpan user ke database
POST /login
Tugasnya:
- Cek email/username
- Bandingkan password (bcrypt compare)
- Generate JWT kalau valid
Dan ya… konsep token kamu itu sudah benar.
Konsep Generate JWT
Snippet kamu sudah tepat secara konsep.
Contoh yang sedikit dirapikan:
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.ID,
"exp": time.Now().Add(time.Hour * 2).Unix(),
})
tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
if err != nil {
return "", err
}
Perbaikan penting:
Jangan hardcode “rahasia_negara” di production.
Masukin ke .env:
JWT_SECRET=super_secret_key
Karena kalau kamu push ke GitHub… ya wassalam
Struktur Implementasi yang Ideal
Supaya sesuai arsitektur layered tadi:
internal/
├── handlers/
│ └── auth_handler.go
├── services/
│ └── auth_service.go
├── models/
│ └── user.go
Flow-nya:
Handler → Service → Repository (via GORM)
Jangan sampai logic JWT ada di handler semua.
Nanti jadi mie goreng tanpa piring.
Contoh Flow Login yang Proper
Handler
- Parse JSON
- Panggil service.Login()
- Return token
Service
- Cari user di DB
- Compare password
- Generate JWT
- Return token
Separation of concern terjaga. Hidup damai.
Kenapa JWT Cocok di Sini?
Karena:
- Stateless
- Cocok buat REST API
- Mudah dipakai mobile / frontend SPA
- Tidak perlu session storage di server
Tiap request cukup kirim:
Authorization: Bearer <token>
Proteksi Route dengan Middleware
Login tanpa proteksi route itu kayak bikin pagar tapi nggak dikunci.
Kita butuh middleware yang:
- Ambil header
Authorization - Validasi format
Bearer <token> - Verifikasi signature JWT
- Cek expiration
- Ambil
user_id - Simpan ke context supaya bisa dipakai handler
Kalau gagal?
Langsung 401 Unauthorized.
Struktur File
Tambahkan file baru:
internal/middleware/
auth_middleware.go
Implementasi AuthMiddleware
package middleware
import (
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
c.Abort()
return
}
// Expect: Bearer <token>
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"})
c.Abort()
return
}
tokenString := parts[1]
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
c.Abort()
return
}
userID := claims["user_id"]
// Simpan user_id ke context
c.Set("user_id", userID)
c.Next()
}
}Nah ini sudah proper middleware, bukan cuma teori
Cara Pakai Middleware
Di main.go:
auth := r.Group("/api")
auth.Use(middleware.AuthMiddleware())
auth.GET("/transactions", transactionHandler.GetTransactions)
Artinya:
Semua route di dalam group itu WAJIB punya token valid.
Cara Ambil user_id di Handler
Di handler transaksi nanti:
userID, _ := c.Get("user_id")
Sekarang kamu bisa query:
WHERE user_id = ?
Dan boom
User A nggak bisa lihat transaksi User B.
Kenapa Ini Penting Secara Arsitektur?
Karena:
- Auth logic tetap terpisah
- Handler bersih
- Business logic nggak campur urusan token
- Mudah diskalakan
Ini sudah best practice REST API modern.
Membangun API Transaksi Keuangan
Sekarang kita bikin fitur utama: CRUD Transaksi. Tapi dengan satu aturan sakral:
User cuma boleh lihat & ubah data miliknya sendiri.
Dan itu kita jaga pakai user_id dari middleware JWT tadi.
Struktur File
Tambahin file:
internal/
├── handlers/
│ └── transaction_handler.go
├── services/
│ └── transaction_service.go
└── models/
└── transaction.goLayer tetap dijaga. Jangan mendadak barbar taruh semua di handler.
Model Transaction
internal/models/transaction.go
package models
import "time"
type Transaction struct {
ID uint `gorm:"primaryKey"`
UserID uint
Amount float64
Type string
Description string
Date time.Time
}Simple. Bersih. Realistis.
POST /transactions
Buat Transaksi Baru
Handler
func (h *TransactionHandler) Create(c *gin.Context) {
var input models.Transaction
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(400, gin.H{"error": "Invalid input"})
return
}
userID, _ := c.Get("user_id")
input.UserID = uint(userID.(float64))
if err := h.service.Create(&input); err != nil {
c.JSON(500, gin.H{"error": "Failed to create transaction"})
return
}
c.JSON(201, input)
}Di sini penting banget:UserID tidak boleh diambil dari request body.
Harus dari token.
Kalau nggak… orang bisa kirim user_id: 2 dan lihat data orang lain. Tamat.
GET /transactions
Ambil Semua Transaksi User
func (h *TransactionHandler) GetAll(c *gin.Context) {
userID, _ := c.Get("user_id")
transactions, err := h.service.GetByUser(uint(userID.(float64)))
if err != nil {
c.JSON(500, gin.H{"error": "Failed to fetch transactions"})
return
}
c.JSON(200, transactions)
}Service:
func (s *TransactionService) GetByUser(userID uint) ([]models.Transaction, error) {
var transactions []models.Transaction
err := s.db.Where("user_id = ?", userID).Find(&transactions).Error
return transactions, err
}Nah ini dia kuncinya:
WHERE user_id = ?Itu tembok pembatas antar user.
DELETE /transactions/:id
Hapus Transaksi (Dengan Validasi Kepemilikan)
Ini bagian yang sering salah. Jangan langsung delete berdasarkan ID.
func (s *TransactionService) Delete(id uint, userID uint) error {
return s.db.
Where("id = ? AND user_id = ?", id, userID).
Delete(&models.Transaction{}).Error
}Kenapa pakai dua kondisi? Karena kalau cuma:
DELETE FROM transactions WHERE id = 5User A bisa hapus transaksi User B.
Dengan:
WHERE id = ? AND user_id = ?
User cuma bisa hapus miliknya sendiri.
Itu best practice security level API.
Route Setup
Di main.go:
protected := r.Group("/api")
protected.Use(middleware.AuthMiddleware())
protected.POST("/transactions", transactionHandler.Create)
protected.GET("/transactions", transactionHandler.GetAll)
protected.DELETE("/transactions/:id", transactionHandler.Delete)
Sekarang semua endpoint transaksi aman.
Kenapa Flow Ini Penting?
Karena kamu sudah menerapkan:
- JWT Auth
- Middleware
- Ownership validation
- Clean layered architecture
- Query filtering by user_id
Ini bukan lagi tutorial receh.
Ini sudah struktur backend production-ready versi MVP.
Urutan kamu sekarang:
- Arsitektur
- Database
- Setup
- Auth
- Middleware
- CRUD Transaksi
Masih rapi. Masih sehat. Masih waras 😆
Kalau next kamu tiba-tiba bahas Kubernetes… itu baru loncat galaksi.
Mau lanjut Section 9:
- Refactor & Error Handling?
- Atau langsung Testing API pakai Postman? 🔥
Frontend: HTML, CSS, dan Vanilla JS
Autentikasi: JWT
Untuk sistem autentikasi, kita menggunakan JSON Web Token.
JWT memungkinkan:
- Autentikasi berbasis token
- Backend bersifat stateless
- Setiap request membawa identitas user
- Sistem lebih fleksibel untuk digunakan di berbagai platform
Library yang digunakan adalah golang-jwt/jwt yang populer dan banyak dipakai di komunitas Go.
Frontend: HTML, CSS, dan Vanilla JavaScript
Frontend akan dibuat menggunakan teknologi dasar web tanpa framework tambahan.
Tujuannya:
- Memahami cara kerja komunikasi HTTP secara langsung
- Mengontrol penuh request ke backend
- Menunjukkan pemahaman fundamental web
Pendekatan ini juga memperjelas bahwa backend yang kita bangun benar-benar terpisah dari frontend.
Tools Pendukung
Beberapa tools yang akan membantu proses pengembangan:
- Postman atau Insomnia untuk menguji endpoint API sebelum dihubungkan ke frontend
- VS Code sebagai editor kode
- Git untuk version control
- Browser modern untuk pengujian antarmuka
Tools ini bukan bagian dari aplikasi, tetapi penting dalam workflow pengembangan profesional.
Arsitektur Sistem
Dalam proyek ini, kita tidak hanya fokus membuat aplikasi yang berjalan, tetapi juga memastikan struktur kodenya rapi, mudah dikembangkan, dan tidak berantakan ketika fitur mulai bertambah.
Untuk itu, kita akan menggunakan pendekatan Layered Architecture pada sisi backend.
Pendekatan ini memisahkan tanggung jawab setiap bagian sistem ke dalam layer yang berbeda, sehingga masing-masing layer memiliki peran yang jelas dan tidak saling tumpang tindih.
Kenapa Tidak Semua Kode Ditulis dalam Satu File?
Tanpa arsitektur yang jelas, proyek kecil memang terlihat sederhana. Namun begitu fitur bertambah, kode bisa berubah menjadi sulit dibaca, sulit diuji, dan sulit diperbaiki.
Dengan layered architecture, kita memastikan:
- Logika bisnis tidak tercampur dengan query database
- Handler hanya fokus menerima dan mengirim response
- Perubahan di database tidak merusak bagian lain
- Kode lebih mudah diuji dan dirawat
Pendekatan ini sangat umum digunakan dalam proyek backend profesional.
Struktur Layer yang Digunakan
Backend akan dibagi menjadi beberapa layer utama:
1. Handler (Controller)
Layer ini bertugas menerima request dari client.
Tugasnya meliputi:
- Membaca parameter dari request
- Validasi awal input
- Memanggil service yang sesuai
- Mengembalikan response dalam format JSON
Handler tidak boleh berisi logika bisnis yang kompleks.
2. Service
Service adalah jantung logika aplikasi.
Di sinilah kita akan:
- Menghitung total saldo
- Memproses transaksi
- Mengatur aturan bisnis
- Menentukan valid atau tidaknya suatu operasi
Service akan memanggil repository untuk mengambil atau menyimpan data, tetapi tidak berinteraksi langsung dengan HTTP.
Dengan cara ini, logika bisnis bisa diuji secara terpisah.
3. Repository
Repository bertugas berinteraksi langsung dengan database.
Tugasnya:
- Menjalankan operasi CRUD
- Mengambil data berdasarkan kondisi tertentu
- Mengelola relasi antar tabel
Repository tidak mengetahui apa itu HTTP, tidak tahu apa itu JWT. Dia hanya tahu cara berbicara dengan database.
Alur Request dalam Sistem
Agar lebih jelas, berikut gambaran alurnya:

Dengan pola ini, setiap bagian sistem punya tanggung jawab yang jelas.
Struktur Folder yang Akan Digunakan
Supaya makin konkret, kita akan menggunakan struktur folder seperti berikut:
- cmd/
- internal/handler
- internal/service
- internal/repository
- internal/model
- internal/middleware
- config/
Struktur ini membuat proyek lebih modular dan scalable.
Jika suatu saat aplikasi berkembang menjadi lebih besar, struktur ini tetap bisa dipertahankan tanpa perlu refactor besar-besaran.
Desain Database
Database adalah fondasi utama dari aplikasi ini. Seluruh data user dan transaksi akan disimpan secara terstruktur agar konsisten, aman, dan mudah dikelola.
Karena aplikasi ini bersifat multi-user (setiap pengguna punya data masing-masing), kita membutuhkan relasi yang jelas antara akun dan transaksi yang dimilikinya.
Untuk kebutuhan proyek ini, kita menggunakan dua tabel utama:
- users
- transactions
Meskipun terlihat sederhana, struktur ini sudah cukup untuk membangun sistem keuangan pribadi yang solid.
1. Tabel Users
Tabel ini menyimpan data akun pengguna.
Kolom yang digunakan:
- id
Primary key dengan tipe auto increment atau UUID. - username
Nama unik untuk identitas user. - email
Digunakan untuk login dan identifikasi akun. - password
Disimpan dalam bentuk hash, bukan teks asli. - created_at
Waktu pembuatan akun.
Beberapa catatan penting:
- Password tidak boleh disimpan dalam bentuk plain text.
- Email sebaiknya dibuat unique agar tidak ada duplikasi akun.
- id akan menjadi penghubung ke tabel transaksi.
2. Tabel Transactions
Tabel ini menyimpan semua aktivitas keuangan user.
Kolom yang digunakan:
- id
Primary key transaksi. - user_id
Foreign key yang mengarah ke tabel users. - amount
Nominal uang (positif). - type
Menentukan apakah transaksi income atau expense. - description
Keterangan transaksi. - date
Tanggal transaksi dilakukan.
Relasi yang digunakan adalah:
Satu user dapat memiliki banyak transaksi.
One-to-many relationship.
Artinya:
users.id → transactions.user_id
Dengan struktur ini, setiap transaksi selalu terikat pada satu user tertentu.
Relasi Antar Tabel
Secara konseptual, hubungan antar tabel bisa digambarkan seperti ini:
Artinya satu user bisa memiliki banyak transaksi, tetapi satu transaksi hanya boleh dimiliki oleh satu user.
Relasi ini penting untuk menjaga isolasi data antar pengguna. Ketika user melakukan login dan mengakses dashboard, backend hanya akan mengambil transaksi berdasarkan user_id yang sesuai dengan token JWT miliknya.
Pertimbangan Desain
Beberapa keputusan desain yang kita ambil:
- Tidak membuat tabel kategori terpisah untuk menjaga kompleksitas tetap sederhana.
- Tidak menyimpan saldo secara langsung di database. Saldo akan dihitung berdasarkan total income dikurangi total expense.
- Menggunakan tipe data numerik yang presisi untuk amount agar tidak terjadi kesalahan pembulatan.
Pendekatan ini menjaga sistem tetap ringan namun tetap realistis.
Kenapa Desain Ini Sudah Cukup?
Untuk skala aplikasi keuangan pribadi, dua tabel ini sudah memadai.
Jika di masa depan ingin dikembangkan, sistem ini masih bisa diperluas dengan:
- Tabel kategori
- Tabel refresh token
- Tabel audit log
- Soft delete pada transaksi
Desain yang sederhana tetapi extensible jauh lebih baik daripada sistem yang kompleks sejak awal.

Setup Proyek
Yuk, kita mulai bikin foldernya! Buka terminal dan ketik:
mkdir finance-app-go
cd finance-app-go
go mod init finance-app
Jangan lupa install dependensi utamanya:
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u github.com/golang-jwt/jwt/v5
go get -u golang.org/x/crypto/bcrypt
Struktur folder awalnya bakal kayak gini:
finance-app/
├── cmd/
├── internal/
│ ├── handlers/
│ ├── models/
│ └── services/
├── main.go
└── .env
Implementasi Backend REST API (Go + JWT)
Di bagian ini, kita fokus bikin fitur Auth dulu. Kita butuh endpoint /register dan /login.
Saat user login berhasil, backend bakal generate JWT Token. Token ini ibarat “tiket masuk” yang bakal dibawa user setiap kali mau akses fitur lain.
Snippet kode konsep login:
// Konsep sederhana generate token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.ID,
"exp": time.Now().Add(time.Hour * 2).Unix(),
})
tokenString, _ := token.SignedString([]byte("rahasia_negara"))
Proteksi Route dengan Middleware
Nah, ini bagian serunya! Gimana caranya biar orang iseng nggak bisa akses data transaksi orang lain? Kita pakai Middleware.
Middleware itu kayak satpam di gedung. Sebelum request masuk ke fungsi utama, dia dicek dulu tokennya valid atau nggak. Kalau token kadaluarsa atau palsu? Access Denied!
Kita bakal bikin middleware AuthMiddleware di Gin yang mengecek header Authorization: Bearer <token>.
Membangun API Transaksi Keuangan
Setelah aman, saatnya fitur utamanya: CRUD Transaksi.
- POST /transactions: Buat catatan baru (misal: Beli Kopi -20.000).
- GET /transactions: Ambil semua riwayat transaksi user yang sedang login.
- DELETE /transactions/:id: Hapus transaksi yang salah input.
Di sini kita pastikan user cuma bisa lihat dan edit datanya sendiri berdasarkan user_id dari token JWT tadi.
Frontend: HTML, CSS, dan Vanilla JS
Karena backend kita REST API, frontend cukup:
- Kirim request ke API
- Simpan JWT
- Tampilkan data
- Kirim transaksi baru
Tanpa framework. Pure. Bersih. Berotot.
Struktur Folder Frontend
Misalnya kita buat sederhana:
frontend/
├── login.html
├── dashboard.html
├── style.css
└── app.js
Login Page
login.html
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="card">
<h2>Login</h2>
<form id="loginForm">
<input type="text" id="username" placeholder="Username" required />
<input type="password" id="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
</div>
<script src="app.js"></script>
</body>
</html>app.js (Login Logic)
const form = document.getElementById("loginForm");
if (form) {
form.addEventListener("submit", async (e) => {
e.preventDefault();
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const res = await fetch("http://localhost:8080/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (res.ok) {
localStorage.setItem("token", data.token);
window.location.href = "dashboard.html";
} else {
alert(data.error);
}
});
}Di sini kita simpan JWT ke localStorage.
Dashboard Page
dashboard.html
<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h2>Dashboard</h2>
<div class="card">
<h3>Saldo Total: <span id="balance">0</span></h3>
</div>
<div class="card">
<h3>Tambah Transaksi</h3>
<form id="transactionForm">
<input type="text" id="description" placeholder="Deskripsi" required />
<input type="number" id="amount" placeholder="Jumlah" required />
<select id="type">
<option value="income">Income</option>
<option value="expense">Expense</option>
</select>
<button type="submit">Tambah</button>
</form>
</div>
<div class="card">
<h3>Riwayat Transaksi</h3>
<ul id="transactionList"></ul>
</div>
<script src="app.js"></script>
</body>
</html>Ambil Data Transaksi (Dengan Token)
Tambahkan di app.js:
const token = localStorage.getItem("token");
async function loadTransactions() {
const res = await fetch("http://localhost:8080/api/transactions", {
headers: {
"Authorization": "Bearer " + token
}
});
const data = await res.json();
const list = document.getElementById("transactionList");
list.innerHTML = "";
let total = 0;
data.forEach(trx => {
const li = document.createElement("li");
li.textContent = `${trx.description} - ${trx.amount} (${trx.type})`;
list.appendChild(li);
if (trx.type === "income") total += trx.amount;
else total -= trx.amount;
});
document.getElementById("balance").textContent = total;
}
if (document.getElementById("transactionList")) {
loadTransactions();
}
Sekarang dashboard langsung hitung saldo otomatis.
Tambah Transaksi
Masih di app.js:
const trxForm = document.getElementById("transactionForm");
if (trxForm) {
trxForm.addEventListener("submit", async (e) => {
e.preventDefault();
const description = document.getElementById("description").value;
const amount = parseFloat(document.getElementById("amount").value);
const type = document.getElementById("type").value;
await fetch("http://localhost:8080/api/transactions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
},
body: JSON.stringify({ description, amount, type })
});
loadTransactions();
trxForm.reset();
});
}
Sekarang:
- Tambah transaksi > langsung refresh list
- Token otomatis dikirim
- Backend validasi user_id dari JWT
Clean.
CSS Minimalis (Responsive)
style.css
body {
font-family: Arial, sans-serif;
background: #f4f6f9;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.card {
background: white;
padding: 20px;
margin: 10px 0;
width: 100%;
max-width: 400px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
input, select, button {
width: 100%;
padding: 8px;
margin: 5px 0;
}
Responsif?
Yes. HP aman. Laptop aman.
Menghubungkan Frontend ke Backend
Di sisi frontend (JavaScript), kita bakal pakai fungsi fetch() bawaan browser.
Logikanya begini:
- User login -> Backend kasih token.
- Token kita simpan di
localStoragebrowser. - Setiap kali mau request data transaksi, kita ambil token dari
localStoragedan taruh di header request.
// Contoh fetch data dengan token
fetch('http://localhost:8080/transactions', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
Strategi Penanganan Error
Aplikasi yang baik itu nggak gampang crash. Kita harus siap kalau:
- Database lagi down.
- User input data kosong.
- Token expired di tengah jalan.
Kita bakal standarisasi format JSON error response, misalnya:
{
"status": "error",
"message": "Token expired, please login again"
}
Di frontend, kita tangkap error ini dan tampilkan notifikasi yang ramah ke user (misal pakai alert() atau sweetalert).
Clean Code & Struktur Proyek
Seiring proyek membesar, file main.go bakal makin panjang. Kita harus rajin memisahkan logika.
- Jangan taruh query database di dalam Handler.
- Gunakan .env file untuk menyimpan secret key JWT dan koneksi database, jangan di- hardcode di kodingan! Ini penting banget buat keamanan.
Pengujian API & Frontend
Sebelum dipamerin, kita tes dulu dong!
- Backend: Pakai Postman. Coba login, copy tokennya, lalu coba akses endpoint transaksi. Pastikan yang tanpa token ditolak.
- Frontend: Coba login dengan password salah, coba input angka negatif, coba refresh halaman (pastikan session tetap login kalau token masih valid).
Deployment ke Production
Sudah jadi di laptop? Saatnya go public!
Kamu bisa deploy backend Go kamu ke layanan cloud seperti Railway, Render, atau Google Cloud Run. Untuk database, bisa pakai layanan managed database atau Docker container.
Jangan lupa set environment variable di server production kamu ya!
Hasil Akhir & Demo
Dan… Tadaaa!
Aplikasi keuangan kamu sudah jadi. Kamu sekarang punya aplikasi fullstack sendiri yang aman, cepat, dan bisa diandalkan buat ngatur duit.
[GAMBAR 5: Screenshot Aplikasi Jadi]
(Deskripsi Gambar: Screenshot asli aplikasi yang sedang berjalan di browser, menampilkan dashboard dengan data transaksi)
Kamu bisa akses kodingan lengkapnya di repository GitHub [Link Dummy] kalau mau intip-intip lebih dalam.
Kata Penutup
Gimana? Ternyata bikin aplikasi keuangan pakai Go dan JWT nggak seseram yang dibayangkan, kan? Kuncinya cuma satu: mulai aja dulu. Jangan takut salah, karena dari error itulah kita belajar paling banyak.
Semoga artikel ini bermanfaat buat perjalanan koding kamu. Kalau ada pertanyaan atau mau request tutorial berikutnya, langsung aja tulis di kolom komentar ya.