Develop Todo Service
Introduction for Todo app#
The proposed todo-app we are about to develop has a relatively simple interface, roughly as shown below:
It contains the following functions:
- Enter the new todo content and press enter to add it
- For each todo item
- You can toggle the
completed/activestatus - You can delete
- You can double-click to start editing
- You can toggle the
- All todo items with
completedstatus can be made clear.
We need to implement a set of server-side api's and that support:
getTodosto get the currenttodosaddTodoadds a newtodoremoveTodoRemovetodoupdateTodoupdatetodoclearCompletedto clear all completedtodos
Then the frontend reuses the server-side type and interface call code via farrow-api to develop the interface based on react.
Todo Service#
Create server/api/todo.ts file and import the depended modules。
Then the Todo Schema is defined and the types contained in the Todo Schema are extracted by TypeOf. Then, we construct the Todos Schema using the List function of farrow-schema.
Note that the above looks a bit redundant, but each part has its role, for example, description becomes an annotation after codegen to the front-end and is a way to document the api.
The types obtained by the front-end are shown below.
getTodos#
Next, we can define the first interface getTodos.
Although getTodos does not require parameters, the input schema still needs to be set to {}, because farrow-api does not currently support setting interface contracts without parameters.
The output schema of getTodos is { todos }, then we use the in-memory todos array to emulate the database and return { todos } directly within the function body of the getTodos interface.
A simple interface is implemented.
addTodo#
Next, let's define the addTodo interface.
The addTodo interface, which contains a non-empty input schema and a non-empty output schema, is a bit more complex than the implementation of the getTodos interface.
When declaring the Todo Schema via class Todo extends ObjectType, we used the Todo.name that comes with the class to access the class name.
However, the AddTodoOutput of const AddTodoOutput = Union(InvalidAddTodoInput, AddTodoSuccess); is a normal variable name and requires the displayName property to be configured.
This property is not required and will work without it. However, AddTodoOutput is inlined at codegen time everywhere it is used, and it is difficult for the front-end to index it directly by type name.
After configuring displayName, the code is generated as follows.
Another thing worth emphasizing is that we have used Discriminated Unions or Tagged Unions to express the Success/Failure mutually exclusive relationship instead of using the Http status code.
We can consume the data as follows:
With Tagged Unions, the back-end interface can force the front-end to handle different cases, and TypeScript will constrain how the front-end consumes the data through type checking.
removeTodo#
Next, we add the removeTodo interface, which is similar to the addTodo interface.
Define the input schema, in this case RemoveTodoInput
Define output schemas, which are merged into output schema by Tagged Unions
Use input schema + output schema to define the api schema
Implement this interface
It is worth mentioning that the Schema.create(value) method, such as RemoveTodoSuccess.create, does not actually do anything special internally, it just returns value.
The purpose of the create method is to make the type derivation more accurate, as we often use Literal String Type as the type of Tag fields when using the Tagged Unions pattern. Using a literal directly, such as RemoveTodoSuccess, will often be derived as a string type. With the help of the create method, we can derive the Literal type more accurately.
updateTodo#
Next, we implement the updateTodo interface.
The interesting part here is how to define an optional field?
With ?= together with Nullable(X) we can define an optional field.
clearCompleted#
Implement the clearCompleted interface.
Ease as getTodos。
ApiService#
In fact, the interfaces getTodos, addTodo, removeTodo, etc. implemented above are essentially functions, but through the Schema definition, we can derive the TypeScript type and get some metadata in runtime as well.
They are just functions, we need to bind it to an http server, which is done by the following code.
ApiService can convert a set of entries functions into farrow-http router objects that can be hooked up to an HttpPipeline.
At this point, we have completed the server-side interface part of TodoService, so let's take a look at the front-end part of consuming the interface data.