Skip to content
This repository was archived by the owner on Jan 6, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
POSTGRESQL_HOST=localhost
POSTGRESQL_PORT=5432
POSTGRESQL_USERNAME=postgres
POSTGRESQL_PASSWORD=admin
POSTGRESQL_DATABASE=togos
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
run:
nodemon --exec "go run" cmd/webservice/main.go
78 changes: 58 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,68 @@
### Requirements

- Implement one single API which accepts a todo task and records it
- Implement one single API which accepts a todo task and records it
- There is a maximum **limit of N tasks per user** that can be added **per day**.
- Different users can have **different** maximum daily limit.
- Write integration (functional) tests
- Write unit tests
- Choose a suitable architecture to make your code simple, organizable, and maintainable
- Write a concise README
- How to run your code locally?
- A sample “curl” command to call your API
- How to run your unit tests locally?
- What do you love about your solution?
- What else do you want us to know about however you do not have enough time to complete?

### Notes
### Choose a suitable architecture to make your code simple, organizable, and maintainable

## Ben Johnson purposes 4 principles to structure our code.

1. Root Package is for domain types
2. Group subpackages by dependency
3. Use a shared mock subpackage
4. Main package ties together dependencies

- Ref links: https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1

- We're using Golang at Manabie. **However**, we encourage you to use the programming language that you are most comfortable with because we want you to **shine** with all your skills and knowledge.
## How to run your code locally?

### How to submit your solution?
1. Download and Install PgAdmin4 tools (postgresql): https://www.postgresql.org/download/
2. Open queries.sql file, then Excute from "--> START" to "--< END" to create "users", "todos" tables
3. Download/Update go modules
```bash
go mod tidy
```
4. Start webservice
```bash
go run cmd/webservice/main.go
```
5. Send request with curl or Postman
```bash
curl -XPOST 'http://localhost:3000/api/lawtrann/todos' -H 'Content-Type: application/json' -d '{"description":"todo something"}'
```

- Fork this repo and show us your development progress via a PR
## A sample “curl” command to call your API
```bash
curl -XPOST 'http://localhost:3000/api/lawtrann/todos' -H 'Content-Type: application/json' -d '{"description":"todo something"}'
```

### Interesting facts about Manabie
## How to run your unit tests locally?
- Unit Test
```bash
go test -v ./services/
```

- Monthly there are about 2 million lines of code changes (inserted/updated/deleted) committed into our GitHub repositories. To avoid **regression bugs**, we write different kinds of **automated tests** (unit/integration (functionality)/end2end) as parts of the definition of done of our assigned tasks.
- We nurture the cultural values: **knowledge sharing** and **good communication**, therefore good written documents and readable, organizable, and maintainable code are in our blood when we build any features to grow our products.
- We have **collaborative** culture at Manabie. Feel free to ask trieu@manabie.com any questions. We are very happy to answer all of them.
- Test cases
- Testcase#1: Create NewUser with Description:"Todo 1"
```bash
curl -XPOST 'http://localhost:3000/api/newuser/todos' -H 'Content-Type: application/json' -d '{"description":"Todo 1"}'
```
- Testcase#2: add Description:"Todo 2" for NewUser on testcase#1
```bash
curl -XPOST 'http://localhost:3000/api/newuser/todos' -H 'Content-Type: application/json' -d '{"description":"Todo 2"}'
```
- Testcase#3: add bunch of Description:"Todo n" until getting "you have reached the limit of adding todo task per day" message
```bash
curl -XPOST 'http://localhost:3000/api/newuser/todos' -H 'Content-Type: application/json' -d '{"description":"todo n"}'
```

Thank you for spending time to read and attempt our take-home assessment. We are looking forward to your submission.
## What do you love about your solution?
- Testability
- With such a pluggable system, we can test the functionality of each layer separately by injecting a mock version of the dependent layers
- Clear separation between layers
- In our domain package we can see an interface for each layer in our application. This helps us to have a clear boundary between each layer.

## What else do you want us to know about however you do not have enough time to complete?
- I haven't dealt with the system's logging yet, nor have I used middleware in this project.
- Using route open source like Chi to optimize router.
- Using docker to run postgredb instead of creating manually through queries.sql file.
87 changes: 87 additions & 0 deletions cmd/webservice/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main

import (
"fmt"
"log"
"os"
"strconv"

"github.com/joho/godotenv"
"github.com/lawtrann/togo/http"
"github.com/lawtrann/togo/postgres"
"github.com/lawtrann/togo/service"
)

func main() {
// Get enviroment variables
if _, err := os.Stat(".env"); err == nil {
err = godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file")
os.Exit(1)
}
}

// Start Main
m := NewMain()

// Open database
m.DB.DSN = GetDns()
if err := m.DB.Open(); err != nil {
fmt.Println(err)
panic(err)
}

// Instantiate repository.
// UserRepository
userRepo := postgres.NewUserRepo(m.DB)
// UserRepository
todoRepo := postgres.NewTodoRepo(m.DB)

// Instantiate business services.
// UserService
userService := service.NewUserService(userRepo)
// TodoService
todoService := service.NewTodoService(todoRepo)
todoService.UserService = userService

// Attach underlying services to the HTTP server.
m.HTTPServer.TodoService = todoService

// Start the HTTP server.
fmt.Println("Running on ... localhost:3000")
m.HTTPServer.ListenAndServe(":3000")
}

// Main represents the program.
type Main struct {
// Postgres database used by Postgres service implementations.
DB *postgres.DB

// HTTP server for handling HTTP communication.
// Postgres services are attached to it before running.
HTTPServer *http.Server
}

// NewMain returns a new instance of Main.
func NewMain() *Main {
return &Main{
DB: postgres.NewDB(""),
HTTPServer: http.NewServer(),
}
}

func GetDns() string {
host := os.Getenv("POSTGRESQL_HOST")
port, err := strconv.Atoi(os.Getenv("POSTGRESQL_PORT"))
if err != nil {
log.Fatalf("Postgres port %s is not valid", os.Getenv("POSTGRESQL_PORT"))
os.Exit(1)
}
user := os.Getenv("POSTGRESQL_USERNAME")
password := os.Getenv("POSTGRESQL_PASSWORD")
dbname := os.Getenv("POSTGRESQL_DATABASE")

return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
}
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/lawtrann/togo

go 1.18

require (
github.com/go-chi/chi v1.5.4
github.com/joho/godotenv v1.4.0
github.com/lib/pq v1.10.6
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
96 changes: 96 additions & 0 deletions http/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package http

import (
"encoding/json"
"io"
"net/http"
"time"

"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/lawtrann/togo"
)

// Server represents an HTTP server. It is meant to wrap all HTTP functionality used by the application
// so that dependent packages (such as cmd/webservice) don't need to reference the "net/http" package at all.
type Server struct {
// A Server defines parameters for running an HTTP server.
Server *http.Server
// Mux is a simple HTTP route multiplexer that parses a request path, records any URL params, and executes an end handler
Router *chi.Mux

// Servics used by the various HTTP routes.
TodoService togo.TodoService
}

func NewServer() *Server {
// Create a new server that wraps the net/http server & add a chi router.
s := &Server{
Server: &http.Server{},
Router: chi.NewRouter(),
}

// RequestID is a middleware that injects a request ID into the context of each request.
s.Router.Use(middleware.RequestID)
// Logger is a middleware that logs the start and end of each request.
s.Router.Use(middleware.Logger)
// Recoverer is a middleware that recovers from panics, logs the panic, and returns a HTTP 500 status if possible.
s.Router.Use(middleware.Recoverer)
// Stop processing after 2.5 seconds.
s.Router.Use(middleware.Timeout(2500 * time.Millisecond))
// CleanPath middleware will clean out double slash mistakes from a user's request path.
s.Router.Use(middleware.CleanPath)
// Allowed content type.
s.Router.Use(middleware.AllowContentType("application/json"))

// Setup error handling routes.
s.Router.NotFound(s.HandleNotFound)
// Setup method not allowed.
s.Router.MethodNotAllowed(http.HandlerFunc(s.HandleMethodNotAllowed))

// Register api routes.
s.Router.Mount("/api", s.RegisterTodoRoutes())

return s
}

// Handles requests to routes that don't exist.
func (s *Server) HandleNotFound(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
// Encode response as JSON
resj := togo.TemplateResponse{
Status: http.StatusNotFound,
Message: togo.ErrPageNotFound.Error(),
}
err := encodeResponseAsJSON(resj, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
}
}

// Handles requests to routes that method is not allowed.
func (s *Server) HandleMethodNotAllowed(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
// Encode response as JSON
resj := togo.TemplateResponse{
Status: http.StatusMethodNotAllowed,
Message: togo.ErrHTTPMethodNotAllowed.Error(),
}
err := encodeResponseAsJSON(resj, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
}
}

func (s *Server) ListenAndServe(port string) error {
return http.ListenAndServe(port, s.Router)
}

// Utility functions
func encodeResponseAsJSON(data interface{}, w io.Writer) error {
return json.NewEncoder(w).Encode(data)
}
97 changes: 97 additions & 0 deletions http/todo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package http

import (
"context"
"encoding/json"
"net/http"

"github.com/go-chi/chi"
"github.com/lawtrann/togo"
)

// RegisterTodoRoutes is a helper function for registering all todo routes.
func (s *Server) RegisterTodoRoutes() *chi.Mux {
r := chi.NewRouter()

// API endpoint for creating user if not existed
r.Route("/{username}", func(r chi.Router) {
r.Use(s.UserCtx)
// API endpoint for creating todo.
r.Post("/todos", s.HandlerTodoAdd)
})

return r
}

// This struct represent a key/value for username path parameter
type userName struct{}

func (s *Server) UserCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username := chi.URLParam(r, "username")
// attach username to context
ctx := context.WithValue(r.Context(), userName{}, username)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

func (s *Server) HandlerTodoAdd(w http.ResponseWriter, r *http.Request) {
// Parse
var todo *togo.Todo
if err := json.NewDecoder(r.Body).Decode(&todo); err != nil || todo.Description == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnprocessableEntity)
// Encode response as JSON
resj := togo.TemplateResponse{
Status: http.StatusUnprocessableEntity,
Message: togo.ErrCouldNotParseObject.Error(),
}
err := encodeResponseAsJSON(resj, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
}
return
}

userName, ok := r.Context().Value(userName{}).(string)
if !ok {
w.WriteHeader(http.StatusUnprocessableEntity)
w.Write([]byte("Could not parse username"))
return
}

// Handle with TodoService
res, err := s.TodoService.Add(r.Context(), todo, userName)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Encode response as JSON
resj := togo.TemplateResponse{
Status: http.StatusOK,
Message: err.Error(),
}
err = encodeResponseAsJSON(resj, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
return
}

// Render output to the client based on JSON
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
resj := togo.TemplateResponse{
Status: http.StatusCreated,
Message: togo.ErrSuccessAddingNewTodo.Error(),
Data: *res,
}
err = encodeResponseAsJSON(resj, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
}
Loading