preface

As a weakly typed language, JS is often “discriminated” by Java, C# and other established programming languages. In addition, JS can only run on the browser at the beginning of its birth, and is nicknamed as “toy language”. With the early advent of Node.js, JS made inroads into the back-end realm. Now TypeScript has breathed new life into the language, essentially adding optional static typing and class-based object-oriented programming as a superset of JavaScript that fits nicely with the most popular front-end framework, React. At the moment it’s all the rage.

In 2019, we need to fully grasp TypeScript and put it into practice.

This project takes TodoList with clear functions as the entry point, combines React bucket and Antd to create a front-end application with strong code robustness and simple user interface, and builds a high-maintainability back-end service with Koa2 + MongoDB as the core.

TodoList is a best practice for full stack applications. You can try it yourself.

Online access address

Technology stack

  • The front end
    • TypeScript makes JS a strongly typed language
    • React (the most popular front-end framework of the moment)
    • Axios (processing HTTP requests)
    • Ant-design (UI Framework)
    • React-router (handles page routing)
    • Redux (Data State Management)
    • Redux-saga (handling asynchronous actions)
  • The back-end
    • Koa2 (Next generation Web development framework based on Node.js platform)
    • MongoDB (non-relational database)

The function point

  • RESTful interface design
  • HTTP request encapsulation, error handling
  • Componentization, code layering
  • User login and registration
  • Todo keyword query
  • Todo content modification
  • Todo status changed
  • Todo record deleted

Practice analysis

TypeScript

TS basically gives us the ability to type JS variables. It also introduces interfaces, generics, enumerations, classes, decorators, namespaces, and more.

let a: number = 1; // int a = 1;
let b: string = "Hello"; // string b = 'Hello'
let arr: number[] = [1.2.3]; // int arr[] = {1,2,3};
Copy the code

TS can constrain our parameters, variable types, and interface types to avoid unnecessary errors during development.

An interface is exported, using /interface/ userstate. ts as an example

export interfaceUserState { user_id? :string; / /? On behalf of the optionalusername? :string; err_msg? :string;
}
Copy the code

User inherits UserState interface, which will have attribute derivation, while in JS, we need to input user.err_msg ourselves, which is tedious and error-prone.

In React, we mainly use stateless function and stateful class components to build applications, including functions passing, function passing, and class inheritance. Our code robustness went up a notch.

Redux state management

At present, State management is an indispensable part of building single-page applications. Simple applications can use State within components easily and quickly. However, as the complexity of applications increases, data will be scattered among different components, and component communication will become extremely complex. It follows three principles:

  • Component data comes from Store and flows in one direction
  • State can only be changed by triggering an action, which is globally unique by defining actionTypes
  • Reducer is a pure function

Because Reducer can only be a pure function (simply speaking, the return result of a function depends only on its parameters and has no side effects during execution, we call this function a pure function.) While in the Fetch scenario, the Action needs to initiate an asynchronous request, which has side effects. Therefore, we use redux-saga to process the asynchronous Action. After processing, the successful synchronous Action is returned and triggered, which is a pure function, and finally changes the store data.

In the FETCH_TODO example, the data flow is as follows:

Interface design

Due to the use of front-end and back-end separation of development, we use the convention interface for data exchange, and the most popular is RESTful interface, which has the following key points:

  • According to the request purpose, set the corresponding HTTP Method, for example, GET corresponding to Read resources, PUT corresponding to Update resources, POST corresponding to Created resources, DELETE corresponding to DELETE resources, corresponding to database CRUD operations
  • The verb indicates the request mode, and the noun indicates the data source. The plural form is usually used, for example, GET/users/2 to obtain the user whose ID is 2
  • Return the corresponding HTTP status code. Common examples are:
    • 200 OKRequest successful,
    • 201 CREATEDCreated successfully,
    • 202 ACCEPTEDUpdate successful,
    • 204 NO CONTENTDeleted successfully,
    • 401 UNAUTHORIZEDUnauthorized,
    • 403 FORBIDDENAccess is prohibited,
    • 404 NOT FOUNDResources don’t exist,
    • 500 INTERNAL SERVER ERRORAn internal error occurred on the server

Using Todo routing as an example, we can design the following interface

const todoRouter = new Router({
  prefix: "/api/todos"}); todoRouter .get("/:userId/all".async (ctx: Context) => {}) // Get all toDos
  .post("/search".async (ctx: Context) => {}) // Keyword search
  .put("/status".async (ctx: Context) => {}) // Change the status
  .put("/content".async (ctx: Context) => {}) // Modify the content
  .post("/".async (ctx: Context) => {}) / / add Todo
  .delete("/:todoId".async (ctx: Context) => {}); / / delete Todo
Copy the code

Layer code

First look at the server directory:

|--server
  |--db
  |--interface
  |--routes
  |--service
  |--utils
  |--app.ts
  |--config.ts
Copy the code

We focus on db, service and routes.

  • dbEstablish data Model (Model), equivalent to MySQL table building link
  • serviceCall the data model to process the business logic of the database, CURD the database, and return the processed data
  • routesCall the method in the service to process the routing request and set the request response

Those who have learned Java know that an interface can only be invoked at the Controller layer through the Domain layer, DAO layer and Service layer. Our project is similar to this idea. Better logical layering can not only improve the maintenance of the project, but also reduce the degree of coupling. This is especially important in large projects.

Error handling

Using service/user as an example, we define the userService class to handle the business logic of user, where addUser is the method to be invoked when the user is registered.

export default class UserService {
  public async addUser(usr: string, psd: string) {
    try {
      const user = new User({
        usr,
        psd,
      });
      // If usr is duplicated, mongodb throws an exception for duplicate key
      return await user.save();
    } catch (error) {
      throw new Error("User name already exists ( ̄o ̄).zz"); }}}Copy the code

Because we set the usr field to be unique, an exception will be thrown when the user registers by entering a user name that has already been registered. At this point, we catch and throw an exception to the route calling this method, and the routing layer catches the error and returns an HTTP response with the existing user name. This is a typical error handling process.

userRouter.post("/".async (ctx: Context) => {
  const payload = ctx.request.body as IPayload;
  const { username, password } = payload;
  try {
    const data = await userService.addUser(username, password);
    if (data) {
      createRes({
        ctx,
        statusCode: StatusCode.Created, }); }}catch (error) {
    createRes({
      ctx,
      errorCode: 1.msg: error.message, }); }});Copy the code

A unified response

For the return result of the API call, to format the response body, we write a generic function to handle the response in /utils/response.ts.

Returns a set of messages indicating whether the call was successful. Such messages usually have a common message body style.

The common return format is a JSON response body consisting of MSG, error_code, and data:

import { Context } from "koa";
import { StatusCode } from "./enum";

interface IRes {
  ctx: Context; statusCode? :number; data? :any; errorCode? :number; msg? :string;
}

const createRes = (params: IRes) = > {
  params.ctx.status = params.statusCode! || StatusCode.OK;
  params.ctx.body = {
    error_code: params.errorCode || 0.data: params.data || null.msg: params.msg || ""}; };export default createRes;
Copy the code

When we request GET/API /todos/:userId/all, we GET all the toDos of the specified user, and return the following response body:

{
  "error_code": 0."data": [{"_id": "5e9b0f1b576bd642796dd7d0"."userId": "5e9b0f08576bd642796dd7cf"."content": "Become full stack engineer ~~~"."status": false."__v": 0}]."msg": ""
}
Copy the code

Not only is this more normative, but it also makes it easier for the front end to receive requests and make better judgments or errors

TodoList: GitHub address