🧑‍💻 실무 개발 & 시스템

OpenAPI Specification을 활용한 API 클라이언트 생성 및 Swagger UI 구현

🧑‍💻데브비 2024. 11. 7. 16:09

openapi-generator

OpenAPI Specification (OAS)는 RESTful API를 정의하고 문서화하기 위한 표준 형식입니다. 이 사양은 API의 구조, 동작, 요청 및 응답 형식 등을 기술하는 데 사용됩니다. OpenAPI는 JSON 또는 YAML 형식으로 작성되며, 다음과 같은 주요 요소를 포함합니다:
  1. servers: API가 배포된 URL 및 관련 정보.
  2. paths: API의 각 엔드포인트를 정의합니다. 각 경로는 HTTP 메서드(예: GET, POST)와 함께 요청할 수 있는 자원에 대한 정보를 포함합니다.
  3. components/responses: 각 경로에 대해 요청 형식(매개변수, 본문)과 응답 형식(상태 코드, 본문)의 세부 사항을 제공합니다.
  4. components/schemas: 요청 및 응답 데이터의 구조를 정의합니다. 이를 통해 클라이언트와 서버 간의 데이터 형식을 명확하게 이해할 수 있습니다.
  5. info: API의 버전, 제목, 설명 등과 같은 추가 정보를 포함합니다.

내가 해볼 것은 위에서 언급한 spec을 openapi-generator를 가지고 사용할 수 있는 소스코드로 변경해볼 것이다. 즉 API 요청을하는 소스코드로 만든다는 것이다.

generator는 typescript-axios를 사용해볼 것이다.

정리를 해보자면

STEP

  1. spec을 준비
  2. code generate
  3. generate된 소스코드를 사용해서 RESTful 통신

STEP 1

code generate을 하기위한 spec을 준비한다. 나는 jsonplaceholderAPI spec을 준비했다.

더보기
openapi: 3.0.1
info:
  title: Example API
  description: API for managing users
  version: 1.0.0
servers:
  - url: <https://jsonplaceholder.typicode.com>
paths:
  /users:
    get:
      summary: Get list of users
      operationId: getUsers
      responses:
        '200':
          description: A list of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'
    post:
      summary: Create a new user
      operationId: createUser
      requestBody:
        description: New user data
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewUser'
      responses:
        '201':
          description: User created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'
  /users/{id}:
    get:
      summary: Get user by ID
      operationId: getUserById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: User data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalServerError'
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: John Doe
        email:
          type: string
          example: johndoe@example.com

    NewUser:
      type: object
      required:
        - name
        - email
      properties:
        name:
          type: string
          example: Jane Doe
        email:
          type: string
          example: janedoe@example.com

    Error:
      type: object
      properties:
        success:
          type: boolean
        message:
          type: string

  responses:
    successResponse:
        description: successful request with no data
        content:
          application/json:
            schema:
              type: object
              example: {"status": 200, "success": true, "message": "message"}
    BadRequest:
      description: 잘못된 요청
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            success: false
            message: 잘못된 요청
    InternalServerError:
      description: 서버 에러
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            success: false
            message: 서버 내부 오류

 

STEP 2

Spec을 정의해봤다. 그러면 정의된 Spec을 가지고 이번에는 generator을 사용해서 소스코드로 바꿔줄 것이다.

아래의 명령어를 사용해서 openapi-generator-cli 를 설치해준다.

npm install -g @openapitools/openapi-generator-cli

설치가 됐다면 아까 정의한 spec을 소스코드로 변경해줄 것이다. 명령어는 아래와 같다.

openapi-generator-cli generate -i openapi.yaml -g typescript-axios -o ./src/api

generate

  • i → spec file
  • g → generator name
  • o → output directory

명령어를 실행하면 아래와 같이 generate될 것이다.

📦api
 ┣ 📂.openapi-generator
 ┃ ┣ 📜FILES
 ┃ ┗ 📜VERSION
 ┣ 📜.gitignore
 ┣ 📜.npmignore
 ┣ 📜.openapi-generator-ignore
 ┣ 📜api.ts
 ┣ 📜base.ts
 ┣ 📜common.ts
 ┣ 📜configuration.ts
 ┣ 📜git_push.sh
 ┗ 📜index.ts

generate되고 나면 아래처럼 소스코드를 생성해준다. 그러면 우리는 path에서 정의한 대로 (operationId 를 참조) getUsers도 찾아볼수있다.

더보기
/* tslint:disable */
/* eslint-disable */
/**
 * Example API
 * API for managing users
 *
 * The version of the OpenAPI document: 1.0.0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (<https://openapi-generator.tech>).
 * <https://openapi-generator.tech>
 * Do not edit the class manually.
 */

import type { Configuration } from './configuration';
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
// Some imports not used depending on template conditions
// @ts-ignore
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common';
import type { RequestArgs } from './base';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base';

/**
 * 
 * @export
 * @interface ModelError
 */
export interface ModelError {
    /**
     * 
     * @type {boolean}
     * @memberof ModelError
     */
    'success'?: boolean;
    /**
     * 
     * @type {string}
     * @memberof ModelError
     */
    'message'?: string;
}
/**
 * 
 * @export
 * @interface NewUser
 */
export interface NewUser {
    /**
     * 
     * @type {string}
     * @memberof NewUser
     */
    'name': string;
    /**
     * 
     * @type {string}
     * @memberof NewUser
     */
    'email': string;
}
/**
 * 
 * @export
 * @interface User
 */
export interface User {
    /**
     * 
     * @type {number}
     * @memberof User
     */
    'id'?: number;
    /**
     * 
     * @type {string}
     * @memberof User
     */
    'name'?: string;
    /**
     * 
     * @type {string}
     * @memberof User
     */
    'email'?: string;
}

/**
 * DefaultApi - axios parameter creator
 * @export
 */
export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) {
    return {
        /**
         * 
         * @summary Create a new user
         * @param {NewUser} newUser New user data
         * @param {*} [options] Override http request option.
         * @throws {RequiredError}
         */
        createUser: async (newUser: NewUser, options: RawAxiosRequestConfig = {}): Promise => {
            // verify required parameter 'newUser' is not null or undefined
            assertParamExists('createUser', 'newUser', newUser)
            const localVarPath = `/users`;
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
            let baseOptions;
            if (configuration) {
                baseOptions = configuration.baseOptions;
            }

            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
            const localVarHeaderParameter = {} as any;
            const localVarQueryParameter = {} as any;

    
            localVarHeaderParameter['Content-Type'] = 'application/json';

            setSearchParams(localVarUrlObj, localVarQueryParameter);
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
            localVarRequestOptions.data = serializeDataIfNeeded(newUser, localVarRequestOptions, configuration)

            return {
                url: toPathString(localVarUrlObj),
                options: localVarRequestOptions,
            };
        },
        /**
         * 
         * @summary Get user by ID
         * @param {number} id 
         * @param {*} [options] Override http request option.
         * @throws {RequiredError}
         */
        getUserById: async (id: number, options: RawAxiosRequestConfig = {}): Promise => {
            // verify required parameter 'id' is not null or undefined
            assertParamExists('getUserById', 'id', id)
            const localVarPath = `/users/{id}`
                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
            let baseOptions;
            if (configuration) {
                baseOptions = configuration.baseOptions;
            }

            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
            const localVarHeaderParameter = {} as any;
            const localVarQueryParameter = {} as any;

    
            setSearchParams(localVarUrlObj, localVarQueryParameter);
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};

            return {
                url: toPathString(localVarUrlObj),
                options: localVarRequestOptions,
            };
        },
        /**
         * 
         * @summary Get list of users
         * @param {*} [options] Override http request option.
         * @throws {RequiredError}
         */
        getUsers: async (options: RawAxiosRequestConfig = {}): Promise => {
            const localVarPath = `/users`;
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
            let baseOptions;
            if (configuration) {
                baseOptions = configuration.baseOptions;
            }

            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
            const localVarHeaderParameter = {} as any;
            const localVarQueryParameter = {} as any;

    
            setSearchParams(localVarUrlObj, localVarQueryParameter);
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};

            return {
                url: toPathString(localVarUrlObj),
                options: localVarRequestOptions,
            };
        },
    }
};

/**
 * DefaultApi - functional programming interface
 * @export
 */
export const DefaultApiFp = function(configuration?: Configuration) {
    const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration)
    return {
        /**
         * 
         * @summary Create a new user
         * @param {NewUser} newUser New user data
         * @param {*} [options] Override http request option.
         * @throws {RequiredError}
         */
        async createUser(newUser: NewUser, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
            const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(newUser, options);
            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
            const localVarOperationServerBasePath = operationServerMap['DefaultApi.createUser']?.[localVarOperationServerIndex]?.url;
            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
        },
        /**
         * 
         * @summary Get user by ID
         * @param {number} id 
         * @param {*} [options] Override http request option.
         * @throws {RequiredError}
         */
        async getUserById(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserById(id, options);
            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
            const localVarOperationServerBasePath = operationServerMap['DefaultApi.getUserById']?.[localVarOperationServerIndex]?.url;
            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
        },
        /**
         * 
         * @summary Get list of users
         * @param {*} [options] Override http request option.
         * @throws {RequiredError}
         */
        async getUsers(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<array>> {
            const localVarAxiosArgs = await localVarAxiosParamCreator.getUsers(options);
            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
            const localVarOperationServerBasePath = operationServerMap['DefaultApi.getUsers']?.[localVarOperationServerIndex]?.url;
            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
        },
    }
};

/**
 * DefaultApi - factory interface
 * @export
 */
export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
    const localVarFp = DefaultApiFp(configuration)
    return {
        /**
         * 
         * @summary Create a new user
         * @param {NewUser} newUser New user data
         * @param {*} [options] Override http request option.
         * @throws {RequiredError}
         */
        createUser(newUser: NewUser, options?: RawAxiosRequestConfig): AxiosPromise {
            return localVarFp.createUser(newUser, options).then((request) => request(axios, basePath));
        },
        /**
         * 
         * @summary Get user by ID
         * @param {number} id 
         * @param {*} [options] Override http request option.
         * @throws {RequiredError}
         */
        getUserById(id: number, options?: RawAxiosRequestConfig): AxiosPromise {
            return localVarFp.getUserById(id, options).then((request) => request(axios, basePath));
        },
        /**
         * 
         * @summary Get list of users
         * @param {*} [options] Override http request option.
         * @throws {RequiredError}
         */
        getUsers(options?: RawAxiosRequestConfig): AxiosPromise<array> {
            return localVarFp.getUsers(options).then((request) => request(axios, basePath));
        },
    };
};

/**
 * DefaultApi - object-oriented interface
 * @export
 * @class DefaultApi
 * @extends {BaseAPI}
 */
export class DefaultApi extends BaseAPI {
    /**
     * 
     * @summary Create a new user
     * @param {NewUser} newUser New user data
     * @param {*} [options] Override http request option.
     * @throws {RequiredError}
     * @memberof DefaultApi
     */
    public createUser(newUser: NewUser, options?: RawAxiosRequestConfig) {
        return DefaultApiFp(this.configuration).createUser(newUser, options).then((request) => request(this.axios, this.basePath));
    }

    /**
     * 
     * @summary Get user by ID
     * @param {number} id 
     * @param {*} [options] Override http request option.
     * @throws {RequiredError}
     * @memberof DefaultApi
     */
    public getUserById(id: number, options?: RawAxiosRequestConfig) {
        return DefaultApiFp(this.configuration).getUserById(id, options).then((request) => request(this.axios, this.basePath));
    }

    /**
     * 
     * @summary Get list of users
     * @param {*} [options] Override http request option.
     * @throws {RequiredError}
     * @memberof DefaultApi
     */
    public getUsers(options?: RawAxiosRequestConfig) {
        return DefaultApiFp(this.configuration).getUsers(options).then((request) => request(this.axios, this.basePath));
    }
}
</array</array

STEP 3

소스코드로 변환하기전에 우리는 generate된 소스코드를 가져다 사용하는 소스코드를 작성해야된다. 간단하게 아래처럼 작성해보았다.

//main.ts
import { Configuration, DefaultApi } from './src/api';

const config = new Configuration({
    basePath: '<https://jsonplaceholder.typicode.com>', // Set the base URL here
});

const usersApi = new DefaultApi(config);

async function fetchUsers() {
    try {
        const response = await usersApi.getUsers(); // Method name depends on your OpenAPI spec
        console.log(response.data);
    } catch (error) {
        console.error('Error fetching users:', error);
    }
}

async function createUser() {
    const newUser = {
        id: 0,
        name: 'New User',
        email: 'newuser@example.com',
    };

    try {
        const response = await usersApi.createUser(newUser); // Adjust as per your spec
        console.log(response.data);
    } catch (error) {
        console.error('Error creating user:', error);
    }
}

// Call the functions
fetchUsers();
createUser();

 

실행 시켜보면 아래처럼 결과가 나온다.

[
  {
    id: 1,
    name: 'Leanne Graham',
    username: 'Bret',
    email: 'Sincere@april.biz',
    address: {
      street: 'Kulas Light',
      suite: 'Apt. 556',
      city: 'Gwenborough',
      zipcode: '92998-3874',
      geo: [Object]
    },
    phone: '1-770-736-8031 x56442',
    website: 'hildegard.org',
    company: {
      name: 'Romaguera-Crona',
      catchPhrase: 'Multi-layered client-server neural-net',
      bs: 'harness real-time e-markets'
    }
  },
  ...

swagger-ui

swagger-ui를 사용해서 정의한 spec을 ui로 뽑아보는 작업도 진행해 보겠다.

프로젝트생성

node express를 사용해서ui를 띄어보도록 하겠다. 아래의 명령어를 사용해서 npm project를 만들어준다

npm init

프로젝트를 만든후에는 npm에서 모듈을 다운받아야 된다 아래의 명령어를 사용해서 다운받으면 된다.

npm i express swagger-ui-express yamljs

설치가 되었다면 아래의 소스코드를 사용해서 파일을 생성해주고 node명령어로 실행시켜주면된다.

const express = require('express');
const app = express();
const port = 3000;
const swaggerUi = require('swagger-ui-express');
const YAML = require('yamljs');
const path = require('path');
const swaggerSpec = YAML.load(path.join(__dirname, '../openapi.yaml'))
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));

// 서버 시작
app.listen(port, () => {
  console.log(`Server is running at <http://localhost>:${port}`);
});

결과물