BudgetAPI is a simple API that helps manage and track budgets for personal finance. It allows users to create, view, update, and delete budget entries and track expenses. Built using golang, labstack/echo/v4, golang-jwt/jwt/v5, gomail.v2, gorm/Postgres, Docker, Terraform etc
- Users can register their details in the application
- Once registered, users would get a welcome note via email
- Users can log into the application using their login credentials i.e.
email_idandpassword - Authenticated user can create category, list all categories and delete a specific category by
id - We can associate few existing categories to an authenticated user
- Users can create a custom category during the process of associating categories to themselves
- Users can list out all the user-categories associations
- Users should be able to input their
budgetfor each category of expenses, e.g.Food 70.00 Ranit - Users can view their budgets, update and delete a specific budget
- Users should be able to record expenses or income as a transaction
- Users should be able to see a detail page for their expenses for selected dates or month
- Users should get notified at the end of the day to provide their expenses input
- Users should be notified at the start of the month to prepare their expenses for the new month
- Users should be allowed to invite their others to their account
Project structure:
budgetapi/
├── cmd/api/
│ ├── handlers/
│ │ ├── app_handler.go
│ │ ├── auth_handler.go
│ │ ├── budget_handler.go
│ │ ├── category_handler.go
│ │ ├── handler.go
│ │ └── validate_request_handler.go
│ │
│ ├── middlewares/
│ │ └── auth_middleware.go
│ │
│ ├── requests/
│ │ ├── budget_request.go
│ │ ├── category_request.go
│ │ ├── param_request.go
│ │ └── user_request.go
│ │
│ ├── routes/
│ │ └── endpoints.go
│ │
│ ├── services/
│ │ ├── budget_service.go
│ │ ├── category_service.go
│ │ └── user_service.go
│ │
│ ├── validation/
│ │ └── validation.go
│ │
│ └── main.go
|
├── common/
│ ├── custom_errors/
│ │ └── not_found_error.go
│ │
│ ├── api_response.go
│ ├── db_connection.go
│ ├── jwt.go
│ ├── pagination.go
│ ├── password.go
│ └── scopes.go
|
├── internal/
│ ├── mailer/
│ │ ├── templates/
│ │ │ ├── hello.html
│ │ │ └── welcome.html
│ │ │
│ │ └── mailer.go
│ │
│ ├── migration/
│ │ ├── seeders/
│ │ │ └── category_seeders.go
│ │ │
│ │ └── migrate_up.go
│ │
│ └── models/
│ ├── base_model.go
│ ├── budget.go
│ ├── category.go
│ ├── user_category.go
│ └── user.go
|
├── terraform-database-stack/
│ ├── db_ec2.tf
│ ├── provider.tf
│ ├── variables.tf
│ └── user-data-db.sh
|
├── terraform-application-stack/
│ ├── app_ec2.tf
│ ├── provider.tf
│ ├── variables.tf
│ └── user-data-app.sh
|
├── .env
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── go.mod
├── go.sum
└── README.md
1. Clone the project
git clone --depth 1 -b v1 https://github.com/RhoNit/budgetapi.git
cd budgetapi/2. Create a .env file and set it up with the values (you can go through the .env.sample for ease of reference)
3. Download all the required libraries/modules mentioned in go.mod file
go get . // or you can download all the dependencies mentioned in the go.mod by executing
go mod download // use any one command to fetch all modules/dependencies into your local4. Database Setup/configuration
- You can setup and connect the database using any locally installed postgres database client tool i.e.
PgAdmin 4,TablePlus,DBeaveretc - Or you can create a postgres container using the database configs mentioned in the
.envfile
docker run -itd \
--name <postgres_container_name> \
-p 5432:5432 \
-e POSTGRES_USER=<user_mentioned_in_.env>
-e POSTGRES_PASSWORD=<password_mentioned_in_.env>
-e POSTGRES_DB=<database_mentioned_in_.env> \
postgres:alpine- If you have
docker-composeinstalled in your local, you can spin up a postgres container in localhost using thedbservice mentioned in thedocker-compose.ymlfile
docker-compose up -d db # here `db` is the database service name from `docker-compose.yml` file- Verify if your database container has been started or not
docker ps # list of all running conatainers in the host
docker logs <container_name> # helpgful while troubleshooting an issue 5. Run the ./internal/migration/seeders/category_seeders.go file to seed a list of static data (or categories) into categories table (OPTIONAL)
go run ./internal/migration/seeders/category_seeders.go6. Build+Run or Run the application
go build -o ./mainbin ./cmd/api/main.go
./mainbin // either generate the binary after building the app and then execute the binary
go run ./cmd/api/main.go // or directly run the applicationI already have deployed the application in an EC2 instance. To test the APIs, you can use the public_ip of ec2_instance i.e. 18.118.156.190
{
"base_url": "http://18.118.156.190:3000/api"
}- Endpoint:
{{ base_url }}/auth/register - Method:
POST - Description: Registers a user into the Database.
-
Headers:
Content-Type: application/json
-
Body:
{ "first_name": "Rishit", "last_name": "Awasthi", "email": "rishit.awasthi@gmail.com", "hashed_password": "LOL123" }
- Body:
{
"success": true,
"message": "User Registration successful!",
"data": {
"id": 2,
"created_at": "2025-01-26T22:58:51.699666076Z",
"updated_at": "2025-01-26T22:58:51.699666076Z",
"first_name": "Rishit",
"last_name": "Awasthi",
"gender": null,
"email": "rishit.awasthi@gmail.com",
"categories": null
}
}- Endpoint:
{{ base_url }}/auth/login - Method:
POST - Description: Authenticates a user and provides a JWT token upon successful login.
-
Headers:
Content-Type: application/json
-
Body:
{ "email": "rishit.awasthi@gmail.com", "hashed_password": "LOL123" }
- Body:
{ "success": true, "message": "user login successful", "data": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzM3OTY0NzcxLCJpYXQiOjE3Mzc5NjM4NzF9.Q2kISH2Vjd0wcbM6aLyNHyBTDsXRodDN8ecTfeux45s", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzM3OTc0NjcxLCJpYXQiOjE3Mzc5NjM4NzF9.BwRvA8Q3HVASbDkk4EhvIHcjygWI_H1lz3AyuBxjKlE", "user": { "id": 2, "created_at": "2025-01-26T22:58:51.699666Z", "updated_at": "2025-01-26T22:58:51.699666Z", "first_name": "Rishit", "last_name": "Awasthi", "gender": null, "email": "rishit.awasthi@gmail.com", "categories": null } } }
- Endpoint:
{{ base_url }}/profile/authenticated-user - Method:
GET - Description: Get the user details of current authenticated user
-
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzM3OTMzMjY1LCJpYXQiOjE3Mzc5MzIzNjV9.dcE9Q5hBIQ4iOHazhZvlxGu7l2V9NU_n4FMiE548hPQ
- Body:
{
"success": true,
"message": "Authenticated user",
"data": {
"id": 1,
"created_at": "2025-01-26T14:14:58.252422Z",
"updated_at": "2025-01-26T14:14:58.252422Z",
"first_name": "Sushant",
"last_name": "Shahi",
"gender": null,
"email": "sushant1@gmail.com",
"categories": null
}
}- Endpoint:
{{ base_url }}/categories/all?page=5&page_size=3 - Method:
GET - Description: Get the paginated categories list seeded in the application
-
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzM3OTMzMjY1LCJpYXQiOjE3Mzc5MzIzNjV9.dcE9Q5hBIQ4iOHazhZvlxGu7l2V9NU_n4FMiE548hPQ
-
Query Parameters:
page(integer, required): The page number to fetch.
Example:5page_size(integer, required): The number of items per page.
Example:3
- Body:
{
"success": true,
"message": "categories retrieved",
"data": {
"page_size": 3,
"page": 5,
"Sort": "",
"total_rows": 15,
"total_pages": 5,
"items": [
{
"id": 13,
"created_at": "2025-01-26T19:13:36.072884Z",
"updated_at": "2025-01-26T19:13:36.072884Z",
"name": "Insurance",
"slug": "insurance",
"is_custom": false
},
{
"id": 14,
"created_at": "2025-01-26T19:13:37.018597Z",
"updated_at": "2025-01-26T19:13:37.018597Z",
"name": "Investments",
"slug": "investments",
"is_custom": false
},
{
"id": 15,
"created_at": "2025-01-26T19:13:37.965668Z",
"updated_at": "2025-01-26T19:13:37.965668Z",
"name": "Savings",
"slug": "savings",
"is_custom": false
}
]
}
}- Endpoint:
{{ base_url }}/categories/create - Method:
POST - Description: Create a category
- Headers:
-
Content-Type: application/json -
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzM3OTMzMjY1LCJpYXQiOjE3Mzc5MzIzNjV9.dcE9Q5hBIQ4iOHazhZvlxGu7l2V9NU_n4FMiE548hPQ
-
- Body:
{
"name": "test category 1",
"is_custom": true
}- Body:
{
"success": true,
"message": "category created",
"data": {
"id": 16,
"created_at": "2025-01-27T09:35:41.824037364Z",
"updated_at": "2025-01-27T09:35:41.824037364Z",
"name": "test category 01",
"slug": "test_category_01",
"is_custom": true
}
}- Endpoint:
{{ base_url }}/categories/delete/:id - Method:
DELETE - Description: Delete a category by ID
- Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzM3OTMzMjY1LCJpYXQiOjE3Mzc5MzIzNjV9.dcE9Q5hBIQ4iOHazhZvlxGu7l2V9NU_n4FMiE548hPQ
- Body:
{
"success": true,
"message": "category deleted",
"data": null
}- Endpoint:
{{ base_url }}/users/categories/all - Method:
GET - Description: Get all category lists which are associated with currently authenticated user
-
Headers:
-
Content-Type: application/json -
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzM3OTMzMjY1LCJpYXQiOjE3Mzc5MzIzNjV9.dcE9Q5hBIQ4iOHazhZvlxGu7l2V9NU_n4FMiE548hPQ
-
- Body:
{
"success": true,
"message": "categories retrieved for user: 2 | rishit.awasthi@gmail.com",
"data": {
"page_size": 10,
"page": 1,
"Sort": "",
"total_rows": 15,
"total_pages": 2,
"items": [
{
"id": 6,
"created_at": "2025-01-26T19:13:29.446575Z",
"updated_at": "2025-01-26T19:13:29.446575Z",
"name": "House Rent",
"slug": "house_rent",
"is_custom": false
},
{
"id": 10,
"created_at": "2025-01-26T19:13:33.231589Z",
"updated_at": "2025-01-26T19:13:33.231589Z",
"name": "Travel & Vacations",
"slug": "travel_&_vacations",
"is_custom": false
},
{
"id": 14,
"created_at": "2025-01-26T19:13:37.018597Z",
"updated_at": "2025-01-26T19:13:37.018597Z",
"name": "Investments",
"slug": "investments",
"is_custom": false
}
]
}
}- Endpoint:
{{ base_url }}/users/categories/associate - Method:
POST - Description: Create User-Categories Association (Attach existing categories to user profile)
-
Headers:
-
Content-Type: application/json -
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzM3OTMzMjY1LCJpYXQiOjE3Mzc5MzIzNjV9.dcE9Q5hBIQ4iOHazhZvlxGu7l2V9NU_n4FMiE548hPQ
-
- Body:
{
"category_ids": [10, 6, 14]
}- Body:
{
"success": true,
"message": "3 categories are associated with user: 2 | rishit.awasthi@gmail.com",
"data": null
}- Endpoint:
{{ base_url }}/users/categories/create - Method:
POST - Description: Create custom categories and associate it to currently authenticated user (Attach custom-created category to current user profile)
-
Headers:
-
Content-Type: application/json -
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzM3OTMzMjY1LCJpYXQiOjE3Mzc5MzIzNjV9.dcE9Q5hBIQ4iOHazhZvlxGu7l2V9NU_n4FMiE548hPQ
-
- Body:
{
"name": "test category"
}- Body:
{
"success": true,
"message": "custom category created",
"data": {
"id": 16,
"created_at": "2025-01-23T16:39:25.982295981+05:30",
"updated_at": "2025-01-23T16:39:25.982295981+05:30",
"name": "test category",
"slug": "test_category",
"is_custom": false
}
}- Endpoint:
{{ base_url }}/budgets/create - Method:
POST - Description: Budget creation for a user
-
Headers:
-
Content-Type: application/json -
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzM3OTMzMjY1LCJpYXQiOjE3Mzc5MzIzNjV9.dcE9Q5hBIQ4iOHazhZvlxGu7l2V9NU_n4FMiE548hPQ
-
- Body:
{
"title": "Bijlee Bill @At Home PG/Jan, 2025 + Netflix Subscription",
"category_ids": [7,11],
"amount": 310
}- Body:
{
"success": false,
"message": "budget with user_id 2, title bijlee_bill_@at_home_pg/jan,_2025_+_netflix_subscription, year 2025 and month 1 is already there in the database"
}- Endpoint:
{{ base_url }}/budgets/all - Method:
GET - Description: List all the created budgets of currently authenticated user
-
Headers:
-
Content-Type: application/json -
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZXhwIjoxNzM3OTMzMjY1LCJpYXQiOjE3Mzc5MzIzNjV9.dcE9Q5hBIQ4iOHazhZvlxGu7l2V9NU_n4FMiE548hPQ
-
- Body:
{
"success": true,
"message": "budgets retrieved",
"data": {
"page_size": 10,
"page": 1,
"Sort": "",
"total_rows": 1,
"total_pages": 1,
"items": [
{
"id": 1,
"created_at": "2025-01-26T23:04:31.083471Z",
"updated_at": "2025-01-26T23:04:31.092958Z",
"title": "Bijlee Bill @At Home PG/Jan, 2025 + Netflix Subscription",
"slug": "bijlee_bill_@at_home_pg/jan,_2025_+_netflix_subscription",
"description": null,
"user_id": 2,
"amount": 310,
"categories": [
{
"id": 7,
"created_at": "2025-01-26T19:13:30.393429Z",
"updated_at": "2025-01-26T19:13:30.393429Z",
"name": "Electricity Bill",
"slug": "electricity_bill",
"is_custom": false
},
{
"id": 11,
"created_at": "2025-01-26T19:13:34.179714Z",
"updated_at": "2025-01-26T19:13:34.179714Z",
"name": "OTT Subscription",
"slug": "ott_subscription",
"is_custom": false
}
],
"date": "2025-01-26T23:04:31.077758Z",
"month": 1,
"year": 2025
}
]
}
}- Created VPC stack along with provisioning subnet, igw, route table associations for Postgres container
- Then using Terraform script spinned up
postgres:14-alpineimage based container inside an EC2 instance provisioned in the above VPC stack
- Using the DB based EC2's public IP, setup the DB connection inside the application
- Created a docker image of application using the Dockerfile
docker build -t <app_img_name> .- Tried to create a multi-stage docker image to reduce the image size.. but encountered some issues when a container was created using that image. Will try to devote some time on that in future.
- Tagged and pushed the docker image in AWS ECR repository
aws configure # first configure with your aws creds then login to the Elasctic Container Registry
aws ecr get-login-password --region <region_name> | docker login --username AWS --password-stdin <ecr_uri>
docker tag <app_img_name>:<tag> <ecr_uri>/<ecr_repo>:<new_tag>
docker push <ecr_uri>/<ecr_repo>:<new_tag>- Create another VPC stack like previous to provision the second EC2 machine where the application's container is gonna be hosted
- Used the ECR repo's image to spin up the application_container
docker run --name <application_container> \
-p <host_port>:<container_port> \
--restart unless-stopped
-d <ecr_uri>/<ecr_repo>:<new_tag>