fastify 및 Typeform을 사용하여 RESTAPI 구축

식당은 제품이 언제 유통되는지 쉽게 추적하고 데이터 구동 방식으로 작업할 수 있도록 재고를 디지털화 관리할 수 있기를 바란다.React Native와 Typescript로 원형을 구축할 기회가 있습니다.
이것이 바로fastify와 TypeORM을 사용하여 백엔드api를 만드는 방법입니다.
Github에서 예제 항목을 찾을 수 있습니다.https://github.com/carlbarrdahl/fastify-server-example

요구 사항

  • 재고는 MSSQL 데이터베이스
  • 에 저장해야 함
  • 데이터베이스와 통신하는 REST api
  • 권한 있는 사용자만 api에 접근할 수 있음
  • 우리가 포괄할 내용

  • fastify
  • 를 사용하여 RESTapi 구축
  • 통합 테스트
  • TypeORM과의 데이터베이스 연결
  • 클라이언트 데이터 검증 및 응답을 허용하는 JSON 모드 정의
  • JWT
  • 를 사용하여 끝점 고정
  • Swagger에서 자동으로 생성되는 문서
  • Fastfy 의 REST api


    나는fastify를 서버 프레임워크로api를 작성하기로 결정했다. 왜냐하면 속도가 빠르고 모듈화되며 사용하기 쉽고 테스트하기 쉽기 때문이다.그것은 또 하나의 훌륭한 플러그인 시스템 생태계가 있다. 당신은 자신의 플러그인을 쉽게 작성할 수 있다. 우리는 잠시 후에 볼 수 있다.
    api가 예상대로 실행되도록 확보하는 좋은 방법은 통합 테스트를 작성하는 것이다.테스트 세트에 대한 개발을 통해 우리는 빠른 피드백 순환을 얻었다. 수동으로api를 호출하여api가 예상대로 작동하는지 검사할 필요가 없다.
    내가 지정한 예상 동작부터 시작합니다.
    test("GET /products returns list of products", () => {})
    test("DELETE /products/:id deletes a product", () => {})
    test("GET /inventory returns list of inventory", () => {})
    test("POST /inventory/:id creates a product", () => {})
    test("DELETE /inventory/:id deletes an inventory", () => {})
    test("JWT token is required for endpoints", () => {})
    
    fastify에서 단점을 테스트하기 위해 우리는 inject 시뮬레이션을 사용하여 서버에 대한 요청을 시뮬레이션하고 방법, URL, 헤더와 부하를 전달한 다음에 응답이 우리의 기대에 부합되도록 확보할 수 있다.
    // test/server.test.ts
    import createServer from "../src/server"
    const server = createServer()
    
    test("GET /inventory returns list of inventory", async done => {
      server.inject({ method: "GET", url: `/inventory` }, (err, res) => {
        expect(res.statusCode).toBe(200)
        expect(JSON.parse(res.payload)).toEqual([]) // expect it to be empty for now
        done()
      })
    })
    
    fastify의 플러그인 시스템을 사용하면 필요할 때 더 작은 부분으로 쉽게 분할할 수 있도록 응용 프로그램을 모듈화할 수 있습니다.다음 폴더 구조를 사용합니다.
    /src
      /modules
        /health
          /routes.ts
          /schema.ts
        /product
          /entity.ts
          /routes.ts
          /schema.ts
        /inventory
          /entity.ts
          /routes.ts
          /schema.ts
      /plugins
        /auth.ts
        /jwt.ts
        /printer.ts
      /server.ts
      /index.ts
    /test
      /server.test.ts
    
    재고 노선은 다음과 같습니다.
    // src/modules/inventory/routes.ts
    module.exports = (server, options, next) => {
      server.get(
        "/inventory",
        // we will cover schema and authentication later
        { preValidation: [server.authenticate], schema: listInventorySchema },
        async (req, res) => {
          req.log.info(`list inventory from db`)
          const inventory = [] // return empty array for now to make the test green
    
          res.send(inventory)
        }
      )
      // routes and controllers for create, delete etc.
      next()
    }
    
    우리의 테스트는 지금 녹색이어야 한다. 이것은 좋은 징조이다.
    그러나 빈 그룹의 목록api를 항상 되돌려주는 것은 유용하지 않습니다.데이터 원본을 연결하자!

    TypeORM을 사용하여 데이터베이스에 연결


    ORM이 뭐냐고요?대부분의 데이터베이스에는 서로 다른 통신 방식이 있다.ORM은 이를 통일된 방식으로 규범화하기 때문에 우리는 변경할 필요 없이 다양한 유형의 지원되는 데이터베이스 사이를 쉽게 전환할 수 있다.
    먼저 솔리드 (또는 모델) 를 만듭니다.
    // src/modules/inventory/entity.ts
    @Entity()
    export class Inventory {
      @PrimaryGeneratedColumn("uuid")
      id: string
    
      // Each product can exist in multiple inventory
      @ManyToOne(type => Product, { cascade: true })
      @JoinColumn()
      product: Product
    
      @Column()
      quantity: number
    
      @Column("date")
      expiry_date: string
    
      @CreateDateColumn()
      created_at: string
    
      @UpdateDateColumn()
      updated_at: string
    }
    
    다음은 이 플러그인을 사용하여 데이터베이스에 연결하고 데이터 저장소를 사용하여 장식기를 만들 것입니다.이렇게 하면 우리의 노선에서 쉽게 그것들에 도착할 수 있다.
    // src/plugins/db.ts
    import "reflect-metadata"
    import fp from "fastify-plugin"
    import { createConnection, getConnectionOptions } from "typeorm"
    import { Inventory } from "../modules/inventory/entity"
    
    module.exports = fp(async server => {
      try {
        // getConnectionOptions will read from ormconfig.js (or .env if that is prefered)
        const connectionOptions = await getConnectionOptions()
        Object.assign(connectionOptions, {
          options: { encrypt: true },
          synchronize: true,
          entities: [Inventory, Product]
        })
        const connection = await createConnection(connectionOptions)
    
        // this object will be accessible from any fastify server instance
        server.decorate("db", {
          inventory: connection.getRepository(Inventory),
          products: connection.getRepository(Product)
        })
      } catch (error) {
        console.log(error)
      }
    })
    
    // ormconfig.js
    module.exports = {
      type: "mssql",
      port: 1433,
      host: "<project-name>.database.windows.net",
      username: "<username>",
      password: "<password>",
      database: "<db-name>",
      logging: false
    }
    
    플러그인을 createServer 에 추가하고 쿼리 데이터베이스 경로를 업데이트할 수 있습니다.
    // src/server.ts
    server.use(require("./plugins/db"))
    
    // src/modules/inventory/routes.ts
    const inventory = await server.db.inventory.find({
      relations: ["product"] // populate the product data in the response
    })
    
    테스트를 생산 데이터베이스에 조회하게 하지 않으면, 메모리 테스트 데이터베이스를 설정하든지, 아니면 시뮬레이션만 하든지.테스트 중에 시뮬레이션을 만듭니다.
    // test/server.test.ts
    import typeorm = require('typeorm')
    
    const mockProducts = [{...}]
    const mockInventory = [{...}]
    const dbMock = {
      Product: {
        find: jest.fn().mockReturnValue(mockProducts),
        findOne: jest.fn().mockReturnValue(mockProducts[1]),
        remove: jest.fn()
      },
      Inventory: {
        find: jest.fn().mockReturnValue(mockInventory),
        findOne: jest.fn().mockReturnValue(mockInventory[1]),
        save: jest.fn().mockReturnValue(mockInventory[0]),
        remove: jest.fn()
      }
    }
    typeorm.createConnection = jest.fn().mockReturnValue({
      getRepository: model => dbMock[model.name]
    })
    typeorm.getConnectionOptions = jest.fn().mockReturnValue({})
    
    다음은 인벤토리 생성 경로를 찾는 방법입니다.
    test("POST /inventory/:id creates an inventory", done => {
      const body = { product_id: mockProducts[0].id, quantity: 1 }
      server.inject(
        {
          method: "POST",
          url: `/inventory`,
          payload: body,
          headers: {
            Authorization: `Bearer ${token}`
          }
        },
        (err, res) => {
          expect(res.statusCode).toBe(201)
          // assert that the database methods have been called
          expect(dbMock.Product.findOne).toHaveBeenCalledWith(body.product_id)
          expect(dbMock.Inventory.save).toHaveBeenCalled()
          // assert we get the inventory back
          expect(JSON.parse(res.payload)).toEqual(mockInventory[0])
          done(err)
        }
      )
    })
    
    재고를 만들 때, 우리는 어떻게 정확한 데이터를 보냈는지 알 수 있습니까?

    JSON 모드를 사용하여 요청 확인


    fastify의 또 다른 장점은 json 모델 규범을 사용하는 내장 모델 검증을 제공했다는 것이다.
    왜 이게 중요해?
    우리는 클라이언트가 어떤 데이터를 보냈는지 영원히 알 수 없고, 모든 루트의 요청체를 수동으로 검사해야 하는 것도 원하지 않는다.반대로 우리는 이러한 요청이 어떤 모습일지, 그리고 어떤 응답을 예상할 수 있는지 묘사하고 싶다.클라이언트가 보내는 내용과 패턴이 일치하지 않으면fastify는 자동으로 오류를 던집니다.이것은 불필요한if문장과 섞이지 않고 깨끗하고 이해할 수 있는 코드를 만들 것이다.

    Note: This will not protect against potential xss. A bad actor is still able to send malicious javascript code. We can use fastify-helmet's xssFilter to protect us against this attack.


    검증 외에도 개발자가api를 어떻게 사용하는지 알 수 있도록 이러한 규범에 따라 루트의 Swagger 문서를 자동으로 생성할 수 있습니다.깔끔했어
    이 json 모드는 간단한 대상으로 정의됩니다.다음은 재고 노선 모델입니다.
    const inventorySchema = {
      id: { type: "string", format: "uuid" },
      product_id: { type: "string", format: "uuid" },
      // note the reference to the productSchema ↘
      product: { type: "object", properties: productSchema },
      quantity: { type: "number", min: 1 },
      expiry_date: { type: "string", format: "date-time" },
      created_at: { type: "string", format: "date-time" },
      updated_at: { type: "string", format: "date-time" }
    }
    export const listInventorySchema = {
      summary: "list inventory",
      response: {
        200: {
          type: "array",
          items: {
            properties: inventorySchema
          }
        }
      }
    }
    export const postInventorySchema = {
      summary: "create inventory",
      body: {
        // incoming request body
        type: "object",
        required: ["product_id", "quantity"],
        properties: {
          product_id: { type: "string", format: "uuid" },
          quantity: { type: "integer", minimum: 1 }
        }
      },
      response: {
        201: {
          type: "object",
          properties: inventorySchema
        }
      }
    }
    
    Fastify는 현재 수신된 데이터에 대해 매우 까다롭고 분실되거나 잘못된 데이터가 있는지 알려 줍니다.
    하지만 누구나 API에 액세스할 수 있습니다.다음은 json 웹 영패를 사용하여 유효한 키를 가진 클라이언트로 제한하는 방법을 연구할 것입니다.

    승인


    우리의api를 보호하기 위해서, 우리는 json 웹 영패를 사용할 것입니다.
    https://jwt.io/introduction/
    이것이 바로 JWT다.io는 다음과 같이 말해야 합니다.

    Authorization: This is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token. Single Sign On is a feature that widely uses JWT nowadays, because of its small overhead and its ability to be easily used across different domains.

    Information Exchange: JSON Web Tokens are a good way of securely transmitting information between parties. Because JWTs can be signed—for example, using public/private key pairs—you can be sure the senders are who they say they are. Additionally, as the signature is calculated using the header and the payload, you can also verify that the content hasn't been tampered with.


    이것은 우리가 그것을 사용하여 사용자가 누구인지 검증하고 안전한 방식으로 기밀 데이터를 공유할 수 있다는 것을 의미한다.우리의 예에서, 우리는 그것을 사용하여 사용자를 간단하게 공유할 수 있는 권한을 부여할 것이다.
    우리는fastify 플러그인을 사용하여 라이브러리를 가져오고 인증 영패의 요청 처리 프로그램으로 장식할 것입니다 authenticate.
    // src/plugins/auth.ts
    import fp from "fastify-plugin"
    
    export default fp((server, opts, next) => {
      server.register(require("fastify-jwt"), {
        secret: "change this to something secret"
      })
      server.decorate("authenticate", async (req, res) => {
        try {
          await req.jwtVerify()
        } catch (err) {
          res.send(err)
        }
      })
    
      next()
    })
    
    그리고 모든 요청에서 authenticate 갈고리 preValidation 를 실행하여 jwt가 유효한지 확인합니다.
    내부에서 전송된 권한 수여 헤더 token 를 검색하고 키 서명을 사용했는지 확인합니다.
    // src/modules/inventory/routes.ts
    server.post(
      "/inventory",
      // authenticate the request before we do anything else
      { preValidation: [server.authenticate], schema: postInventorySchema },
      async (req, res) => {
        const { quantity, product_id } = req.body
        req.log.info(`find product ${product_id} from db`)
        const product = await server.db.products.findOne(product_id)
    
        if (!product) {
          req.log.info(`product not found: ${product_id}`)
          return res.code(404).send("product not found")
        }
    
        req.log.info(`save inventory to db`)
        const inventory = await server.db.inventory.save({
          quantity,
          product,
          expiry_date: addDays(product.expires_in)
        })
    
        res.code(201).send(inventory)
      }
    )
    
    현재 사용자 계정이 없으므로 다음과 같은 임시 토큰을 생성할 수 있습니다.
    server.ready(() => {
      const token = server.jwt.sign({ user_id: "<user_id>" })
      console.log(token)
    })
    
    보시다시피 영패는 Base64 문자열로 인코딩된 서명 대상입니다.우리는 그것을 사용하여 특정 사용자나 사용자가 만든 재고에 대한 접근을 제한할 수 있다.이 가능하다, ~할 수 있다,...
    // src/modules/inventory/routes.ts
    server.get(
      "/inventory/:id",
      { schema: getInventorySchema, preValidation: [server.authenticate] },
      async (req, res) => {
        const inventory = await server.db.inventory.findOne(req.params.id)
        // Make sure the requesting user is the same as the inventory owner
        if (req.user.user_id !== inventory.owner.id) {
          throw new Error("Unauthorized access")
        }
        res.send(inventory)
      }
    )
    
    더 높은 용법은 영패가 보낸 시간 스탬프를 검사할 수 있다(iat.

    거들먹거리는 문서


    내가 줄곧 이야기하고 있는 이 거들먹거리는 문서는 무엇입니까?기본적으로, 이것은 당신의api에 시각화된 인터페이스를 제공하여 작업 방식, 요청체가 어떤 모습인지, 예시 응답을 볼 수 있도록 합니다.이것은 우리가 json 모델에서 정의한 내용과 기본적으로 같고 문서로 공개됩니다.

    이 구성은 createServer 에서 사용됩니다.
    server.register(require("fastify-oas"), {
      routePrefix: "/docs",
      exposeRoute: true,
      swagger: {
        info: {
          title: "inventory api",
          description: "api documentation",
          version: "0.1.0"
        },
        servers: [
          { url: "http://localhost:3000", description: "development" },
          { url: "https://<production-url>", description: "production" }
        ],
        schemes: ["http"],
        consumes: ["application/json"],
        produces: ["application/json"],
        security: [{ bearerAuth: [] }],
        securityDefinitions: {
          bearerAuth: {
            type: "http",
            scheme: "bearer",
            bearerFormat: "JWT"
          }
        }
      }
    })
    

    미래의 개선

  • 사용자 계정
  • 캐시
  • 향상된 오류 처리
  • fastify 투구
  • 를 사용하여 XSS 및 기타 공격에 대한 안전성 향상
  • 로드 밸런싱
  • 너는 이 문장이 어떻다고 생각하니?
    뭐 공부 해요?이해하기 어려운 것이 있습니까?코드가 너무 많아요?아직 부족해?내가 일을 잘못했나?댓글로 알려주세요.

    좋은 웹페이지 즐겨찾기