Skip to main content

Todos

There was a time, when building a todo app was the default demo, so here is one for you.:

Go Service

Service interface defintion

package todos

import "time"

type Error string

const (
ErrCreateEmpty Error = "ErrCreateEmpty"
ErrCreateDuplicate Error = "ErrCreateDuplicate"
ErrNotFound Error = "ErrNotFound"
)

func (e *Error) String() string {
return string(*e)
}

func (e *Error) Error() string {
return e.String()
}

func NewError(err Error) *Error {
return &err
}

type TodoID string

type Todo struct {
ID TodoID `json:"id"`
Text string `json:"text"`
Complete bool `json:"complete"`
Created time.Time `json:"created"`
}

type Todos []Todo

type Service interface {
GetTodos() (todos Todos, err *Error)
CreateTodo(text string) (todos Todos, err *Error)
SetComplete(ID TodoID, complete bool) (todos Todos, err *Error)
DeleteTodo(ID TodoID) (todos Todos, err *Error)
}

Service implementation

package server

import (
"time"

"github.com/foomo/gotsrpc-playground/server/services/todos"
)

type todosService struct {
todos todos.Todos
}

func NewTodos() todos.Service {
return &todosService{
todos: todos.Todos{
todos.Todo{
ID: "hello",
Text: "Play with todo service",
Complete: false,
Created: time.Now(),
},
todos.Todo{
ID: "run-example",
Text: "run example service",
Complete: true,
Created: time.Now(),
},
},
}
}

func (s *todosService) GetTodos() (todos.Todos, *todos.Error) {
return s.todos, nil
}

func (s *todosService) CreateTodo(text string) (updatedTodos todos.Todos, err *todos.Error) {
if text == "500" {
panic("you asked for it ;)")
}
if text == "" {
return nil, todos.NewError(todos.ErrCreateEmpty)
}
for _, todo := range s.todos {
if todo.Text == text {
return nil, todos.NewError(todos.ErrCreateDuplicate)
}
}
s.todos = append(
todos.Todos{
todos.Todo{
ID: todos.TodoID(text),
Text: text,
Complete: false,
Created: time.Now(),
}},
s.todos...,
)
return s.todos, nil
}

func (s *todosService) SetComplete(ID todos.TodoID, complete bool) (updatedTodos todos.Todos, err *todos.Error) {
for i, todo := range s.todos {
if todo.ID == ID {
s.todos[i].Complete = complete
return s.todos, nil
}
}
return nil, todos.NewError(todos.ErrNotFound)
}

func (s *todosService) DeleteTodo(ID todos.TodoID) (updatedTodos todos.Todos, err *todos.Error) {
newTodos := todos.Todos{}
deleted := false
for _, todo := range s.todos {
if todo.ID == ID {
deleted = true
continue
}
newTodos = append(newTodos, todo)
}
if !deleted {
return nil, todos.NewError(todos.ErrNotFound)
}
s.todos = newTodos
return s.todos, nil
}

Next.js TypeScript client

import { DocsAside } from "@/components/DocsAside";
import { SimpleForm } from "@/components/SimpleForm";
import { TransportError } from "@/components/TransportError";
import { TodoError } from "@/components/todos/TodoError";
import { TodoList } from "@/components/todos/TodoList";
import { ServiceClient } from "@/services/generated/client-todos";
import { Error, TodoID, Todos } from "@/services/generated/vo-todos";
import { getClientWithTransportLog } from "@/services/transportWithLog";

import { useEffect, useState } from "react";

const client = getClientWithTransportLog(ServiceClient);

type TodoReturnType = {
todos: Todos | null;
err: Error | null;
};
type TodoClientPromise = Promise<TodoReturnType>;

const Todos = () => {
// local react state:
const [active, setActive] = useState(false);
// list of todos
const [todos, setTodos] = useState<Todos | null>(null);
// method error handling
const [error, setError] = useState<Error | null | undefined>(undefined);
// transport error handling
const [transportError, setTransportError] = useState<string>("");

// all methods of the todo service return the same type
// and thus the resulting promises can be handled with the same function
const handleTodoClientPromise = (promise: TodoClientPromise) => {
// to give the user instant feedback, that he successfully triggered an interaction,
// we reset, the current state and set active to true
setError(null);
setTodos(null);
setActive(true);

// wire up the promise
promise
.then((value: TodoReturnType) => {
// handle a successful response
setError(value.err);
setTodos(value.todos);
})
.catch((e: any) => {
// a transport error is not a business error and has to be handled separately
setTransportError(
"a transport error occurred - please reload the page: " + e
);
})
.finally(() => {
// no matter, if things worked or not, we are not active any more
setActive(false);
});
};

// initial load of todos
useEffect(() => {
handleTodoClientPromise(client.getTodos());
}, []);

return (
<>
<DocsAside examplePage="todos">
This is a simple application, that includes error handling
<ul>
<li>
all responses are slowed down by a server side sleep of 1s - to
illustrate the request / response lifecycle
</li>
<li>
you can trigger a <code>500 server error</code> by entering{" "}
<q>500</q> as a todo
</li>
<li>
submitting an empty todo will trigger an <code>ErrCreateEmpty</code>
</li>
<li>
submitting a duplicate todo will trigger an{" "}
<code>ErrCreateDuplicate</code>
</li>
</ul>
</DocsAside>

<SimpleForm
onCreate={(text) => handleTodoClientPromise(client.createTodo(text))}
placeholder="new todo"
enabled={!active}
/>

{active ? (
<p>service call in progress</p>
) : (
<>
<TransportError error={transportError} />
<TodoError error={error} />
</>
)}

<TodoList
todos={todos}
onDeleteTodo={(id: TodoID) =>
handleTodoClientPromise(client.deleteTodo(id))
}
onCompleteTodo={(id: TodoID, complete: boolean) =>
handleTodoClientPromise(client.setComplete(id, complete))
}
/>
</>
);
};

export default Todos;