The previous chapter introduced the construction of front-end projects, so in this chapter, we will build back-end projects based on Express

Technology stack

express typescript mongoose

Initialize the project

Project initialization

# initialization
yarn init -y

Initialize git repository
git init

# express installation
yarn add express

# installation typescript
# ts-node-dev listen for file changes restart the service and share the result of ts
Install express and Node declaration files
yarn add typescript ts-node-dev @types/express @types/node --dev
Copy the code

Package. json Added startup script

"scripts": {
    "dev": "ts-node-dev --respawn --transpile-only src/app.ts"
},
Copy the code

Root directory new. Gitignore

node_modules
build
Copy the code

Ts environment configuration, which was covered in the previous chapter

New tsconfig. Json

{
  "compilerOptions": {
    "outDir": "build"."target": "es5"."module": "commonjs"."sourceMap": true."esModuleInterop": true."forceConsistentCasingInFileNames": true."strict": true."skipLibCheck": true}}Copy the code

Entry file app.ts

Create a SRC directory and create app.ts in the directory

// src/app.ts

import express from 'express'
import routes from './routes' / / routing

const app = express()

app.use(express.json())

const PORT = 1337

/ / start
app.listen(PORT, async() = > {console.log(`App is running at http://localhost:${PORT}`)
  routes(app)
})

Copy the code

routing

Create the routes directory and create the index.ts directory

// src/routes/index.ts

import { Express, Request, Response, Router } from 'express'

// Route configuration interface
interface RouterConf {
  path: string.router: Router, meta? : unknow }// Route configuration
const routerConf: Array<RouterConf> = []

function routes(app: Express) {
  / / root directory
  app.get('/'.(req: Request, res: Response) = > res.status(200).send('Hello Shinp!!! '))

  routerConf.forEach((conf) = > app.use(conf.path, conf.router))
}

export default routes
Copy the code

Start the project

yarn dev

Localhost :1337
# You can see the browser output Hello Shinp!!
Copy the code

Code specifications (described in the previous chapter, essentially the same)

The reference configuration

// .eslintrc.js

module.exports = {
  root: true.// Extend the rule
  extends: [
    'eslint:recommended'.'plugin:@typescript-eslint/recommended'.'plugin:prettier/recommended'.'prettier',].parserOptions: {
    ecmaVersion: 12.parser: '@typescript-eslint/parser'.sourceType: 'module',},// Register the plug-in
  plugins: ['@typescript-eslint'.'prettier'].// Rules are added as needed
  rules: {
    'no-var': 'error'.'no-undef': 0.'@typescript-eslint/consistent-type-definitions': ['error'.'interface',}}Copy the code
// .prettierrc.js

module.exports = {
  tabWidth: 2.semi: false.singleQuote: true,}Copy the code

Project Common Configuration

Log output

Pino efficient Node.js logger, beautifying the output.

Dayjs is a minimalist date-manipulation JS library.

# installation
yarn add pino pino-pretty dayjs

Install declaration file
yarn add @types/pino --dev
Copy the code

Create logger.ts in the utils directory

// logger.ts

import pino from 'pino'
import dayjs from 'dayjs'

const log = pino({
  transport: { // Pino 7.x is written differently
    target: 'pino-pretty',},base: {
    pid: false,},timestamp: () = > `,"time":"${dayjs().format()}"`,})export default log
Copy the code

Used in app. Ts

// app.ts is truncated
import logger from './utils/logger'

app.listen(POST, async () => {
  logger.info(`App is running at http://localhost:${POST}`)
  routes(app)
})
Copy the code

Global configuration

Config A lightweight and simple NodeJS configuration and deployment library

It is very simple to use, create config in the root directory, and use the API operations anywhere

The config directory is created in the root directory and default.json is created in the root directory

/* config/default.json */
{
  "port": 1337
}
Copy the code

It can also be a TS file

// default.ts

export default {
  port: 1337,}Copy the code

Used in app. Ts

// app.ts is truncated

import config from 'config'

const PORT = config.get<number> ('port')

app.listen(PORT, async () => {
  logger.info(`App is running at http://localhost:${PORT}`)})Copy the code

Unified response parameter

Constants root directory for constants, code. Ts

// constants/code.ts

// Enumeration status code is defined according to your own needs
enum Code {
  success = 3000,
  denied,
  error
}

enum CodeMessage {
  success = 'Request successful',
  denied = 'No permissions',
  error = 'System exception'
}

// The state type can only be the state enumerated in Code
type codeType = keyof typeof Code

export { Code, codeType, CodeMessage }
Copy the code

Create commonres. ts under utils

// utils/commonRes.ts
// Edit as needed

import { Response } from 'express'
import { Code, codeType, CodeMessage } from '.. /constants/code'

interface ResOption {
  type? : codeType status? :numbermessage? : unknow }// The response succeeds by default
function commonRes(res: Response, data: unknown, options? : ResOption) {
  options = Object.assign({ type: Code[3000] }, options || {}) / / success by default

  const { type, status, message } = options
  let resStatus = status

  if (resStatus === undefined) {
    // Set the status code according to the status
    resStatus = type === Code[3000]?200 : 409
  }

  // Response parameters
  const sendRes: { code: number; data: unknown; message? : unknow } = {code: Code[type as codeType],
    data,
  }
  // Response description
  message && (sendRes.message = message)

  return res.status(resStatus).send(sendRes)
}

// Error response
commonRes.error = function (res: Response, data: unknown, message? : unknown, status? :number) {
  logger.error(message || CodeMessage['error'])
  this(res, data, { type: 'error'.message: message || CodeMessage['error'].status: status || 409})}// No permission response
commonRes.denied = function (res: Response, data: unknown) {
  this(res, data, { type: 'denied'.message: CodeMessage['denied'].status: 401})}export default commonRes
Copy the code

Routes used in the

// routes/index.ts

app.get('/'.(req: Request, res: Response) = > {
    // commonRes(res, { word: 'Hello Shinp!!! '}, {type: 'success', message: 'request successful'}) succeeded
    // commonres. denied(res, null) No permission
    // commonres. error(res, null
    commonRes(res, { word: 'Hello Shinp!!! ' }) / / success
})
Copy the code

Browser print

{
    "code": 3000."data": {
        "word": "Hello Shinp!!!"}}Copy the code

Static error capture

// utils/silentHandle.ts

// If there is an error during execution, the first element of the returned array is captured and assigned
async function silentHandle<T.U = Error> (fn: Function. args:Array<unknown>) :Promise"[U.null"|"null.T] >{
  let result: [U, null"|"null, T]

  try {
    result = [null.awaitfn(... args)] }catch (e: any) {
    result = [e, null]}return result
}

export default silentHandle

Copy the code

use

// routes/index.ts

import silentHandle from '.. /utils/silentHandle'

const getInfo = function () {
  return new Promise((resolve, reject) = > {
    setTimeout(() = > {
      Math.random() > . 5 ? resolve('info... ') : reject('error... ')},500)
  })
}

app.get('/'.async (req: Request, res: Response) => {
    const [e, result] = await silentHandle(getInfo)
    e ? commonRes.error(res, null) : commonRes(res, { result })
})
Copy the code

Middleware – Response header information

Middleware is created in the root directory to hold middleware and responseHeader.ts is created in the root directory

// middleware/responseHeader.ts

import { Request, Response, NextFunction } from 'express'

const responseHeader = (req: Request, res: Response, next: NextFunction) = > {
  const { origin, Origin, referer, Referer } = req.headers
  
  // Wildcard if not manually set
  const allowOrigin = origin || Origin || referer || Referer || The '*'
    
  // Allow the request source
  res.header('Access-Control-Allow-Origin', allowOrigin)
  // Allow header fields
  res.header('Access-Control-Allow-Headers'.'Content-Type')
  // Allow public header fields
  res.header('Access-Control-Expose-Headers'.'Content-Disposition')
  // The allowed request mode
  res.header('Access-Control-Allow-Methods'.'PUT,POST,GET,DELETE,OPTIONS')
  // Cookies are allowed
  res.header('Access-Control-Allow-Credentials'.'true')

  // Precheck returns 204
  if (req.method == 'OPTIONS') {
    res.sendStatus(204)}else {
    next()
  }
}

export default responseHeader
Copy the code

Create index. Ts

import { Express } from 'express'
import express from 'express'
import responseHeader from './responseHeader'

function initMiddleware(app: Express) {
  app.use(express.json())
  app.use(responseHeader)
}

export default initMiddleware
Copy the code

App. In the ts

import initMiddleware from './middleware'

const app = express()

// Mount middleware
initMiddleware(app)
Copy the code

mongodb

The installation

Install mongodb on your system and use compass

Connect the mongo

# Mongoose operates mongodb's library
# zod type definition library

yarn add mongoose zod
yarn @types/mongoose --dev
Copy the code

configuration

// config/default.ts

// Add mongodb connection configuration
dbUri: 'mongodb://localhost:27017/shinp'.dbUser: 'root'.dbPassword: '* * * * * *'.dbAuthSource: 'admin'.Copy the code
// utils/dbConnect.ts

/ / db connection

import mongoose from 'mongoose'
import config from 'config'
import logger from './logger'

async function dbConnect() {
  const dbUri = config.get<string> ('dbUri')
  const dbUser = config.get<string> ('dbUser')
  const dbPassword = config.get<string> ('dbPassword')
  const dbAuthSource = config.get<string> ('dbAuthSource')

  try {
    const connection = await mongoose.connect(dbUri, {
      user: dbUser,
      pass: dbPassword,
      authSource: dbAuthSource,
    })

    logger.info('DB connected')

    return connection
  } catch (error) {
    logger.error('Could not connect to db')
    process.exit(1)}}export default dbConnect
Copy the code

Used in app. Ts

// app.ts

import dbConnect from './utils/dbConnect'

app.listen(PORT, async () => {
  logger.info(`App is running at http://localhost:${PORT}`)

  await dbConnect()

  routes(app)
})
Copy the code

If DB Connected is displayed on the console, the connection is successful

Defining data modules

Take user information as an example

Create models/user.model.ts in the root directory

// user.model.ts

import mongoose from 'mongoose'
import config from 'config'

// Template interface
export interface UserDocument extends mongoose.Document {
  name: string
  account: string
  password: string
  createdAt: Date
  updatedAt: Date
  deletedAt: Date
}

// Template verification rule
const userSchema = new mongoose.Schema(
  {
    name: { type: String.required: true },
    account: { type: String.required: true },
    password: { type: String.required: true}, {},timestamps: true,})/ / the only
userSchema.index({ account: 1.deletedAt: 1 }, { unique: true })

The template is automatically created in mongodb after execution
const UserModel = mongoose.model<UserDocument>('User', userSchema)

export default UserModel
Copy the code

Parameter calibration

Create schema/user.schema.ts in the root directory

// user.schema.ts
// Interface parameter verification mainly uses ZOD, specific usage can be viewed in the document

import { object.string, TypeOf } from 'zod'

// Create an interface
export const createUserSchema = object({
  body: object({
    account: string({ required_error: 'Missing account name' }).nonempty(),
    name: string({ required_error: 'Missing user name' }).nonempty(),
    password: string({ required_error: 'Missing user password' }).min(6.'Password too short - at least 6 characters'),
    passwordConfirmation: string({ required_error: 'Confirmation password missing' }),
  }).refine((data) = > data.password === data.passwordConfirmation, {
    message: 'Passwords do not match twice'.path: ['passwordConfirmation',})})export type CreateUserInput = Omit<TypeOf<typeof createUserSchema>, 'body.passwordConfirmation'>
Copy the code

Added a middleware for verifying interface parameters

// middleware/validate.ts

import { Request, Response, NextFunction } from 'express'
import { AnyZodObject } from 'zod'
import { commonRes } from '.. /utils'

const validate = (schema: AnyZodObject) = > (req: Request, res: Response, next: NextFunction) = > {
  try {
    schema.parse({
      body: req.body,
      query: req.query,
      params: req.params,
    })

    next()
  } catch (e: any) {
    return commonRes.error(res, null, e.errors)
  }
}

export default validate
Copy the code

Generic database manipulation functions

Provides a set of common mongodb add, delete, modify and check operation methods

// utils/crudProvider.ts

// Add as needed

import { FilterQuery, UpdateQuery, DocumentDefinition, QueryOptions, Model, InsertManyOptions } from 'mongoose'

class BaseCrudProviderCls<document.Cdocument> {
  private DBModel: Model<any>

  constructor(DBModel: Model<any>) {
    this.DBModel = DBModel
  }

  async create(input: DocumentDefinition<Cdocument>) {
    const data = await this.DBModel.create(input)

    return data.toJSON()
  }

  async update(query: FilterQuery<document>, update: UpdateQuery<document>, options? : QueryOptions) {
    return this.DBModel.updateOne(query, update, options)
  }

  async find(query: FilterQuery<document>, projection? :any, options? : QueryOptions) {
    const result = await this.DBModel.find(query, projection, options)
    return result && result.map((d) = > d.toJSON())
  }
}

const BaseCrudProvider = function <document.Cdocument> (DBModel: Model<any>) {
  const CRUD = new BaseCrudProviderCls<document, Cdocument>(DBModel)

  return {
    create: CRUD.create.bind(CRUD),
    update: CRUD.update.bind(CRUD),
    find: CRUD.find.bind(CRUD),
  }
}

export { BaseCrudProvider }
Copy the code

Database operations

Create service/user.service.ts in the root directory

// user.service.ts
/ / directing operations

import { BaseCrudProvider } from '.. /utils/crudProvider'
import UserModel, { UserDocument } from '.. /models/user.model'

const CRUD = BaseCrudProvider<UserDocument, Omit<UserDocument, 'createdAt'>>(UserModel)

export default CRUD
Copy the code

Interface processing

The root directory to create the controller/user. Controller. Ts

// user.controller.ts

import { Request, Response } from 'express'
import { commonRes, silentHandle } from '.. /utils'

import { CreateUserInput } from '.. /schema/user.schema'
import USER_CRUD from '.. /service/user.service'

export async function createUserHandler(req: Request<{}, {}, CreateUserInput['body']>, res: Response) {
  const [e, user] = await silentHandle(USER_CRUD.create, req.body)

  return e ? commonRes.error(res, null, e.message) : commonRes(res, user)
}
Copy the code

Route definition

Finally, add routes/user.routes.ts

import { Router } from 'express'
import { createUserHandler } from '.. /controller/user.controller'
import validate from '.. /middleware/validate'
import { createUserSchema } from '.. /schema/user.schema'

const router = Router()

// If interface parameters need to be verified, add verification middleware
router.post('/create', validate(createUserSchema), createUserHandler)

export default router
Copy the code

Introduced in routes/index.ts

import User from './user.routes'

const routerConf: Array<RouterConf> = [{ path: '/user'.router: User }]
Copy the code

Start the project debugging interface

Start the project, refresh the database in Compass, and you can see the Users module

Postman calls interface, add a record, can pass in different parameters test parameter verification

Now the mongodb configuration is complete, add modules as required

complete

At this point, the basic framework of the background project is completed, and the subsequent function can be expanded according to their own needs

The above is combined with their own development experience, as well as read other great god’s excellent code construction, mistakes, but also please correct, thank you!!

Some continuous integration related content may be recorded later for those who are interested.