3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

aws-nodejs-typescriptのテンプレートを使ってみる

Last updated at Posted at 2022-01-17

はじめに

業務でServerlessFrameworkを触ることがあり、typescriptのテンプレートを使用すると
serverless.ymlからserverless.tsになったことやビルドツールがwebpackからesbuildになったことにより詰まったので、シェアできればと思い、まとめてみます。

Serverless Frameworkとは

Serverless Frameworkとはなんでしょうか?
https://github.com/serverless/serverless によると、

The Serverless Framework – Build applications on AWS Lambda and other next-gen cloud services, that auto-scale and only charge you when they run. This lowers the total cost of running and operating your apps, enabling you to build more and manage less.

とのことです。英語だとすっと入ってこないので、日本語に訳すと(DeepL翻訳より)

サーバーレスフレームワーク - AWS Lambdaやその他の次世代クラウドサービス上でアプリケーションを構築し、オートスケールで、実行時にのみ課金されるようにします。これにより、アプリケーションの実行と運用にかかる総コストを削減し、より多くの構築とより少ない管理を可能にします。

つまり、Serverless Framework は、AWS Lambda や Amazon API Gateway などのサービスを使用して、サーバーレスアプリケーションを迅速に構築およびデプロイをすることができるフレームワークとなります。

事前準備

実行環境としては、以下の環境で行いました。
serverlessawscliについては、brewコマンドを使ってインストールしております。

❯ node -v
v17.3.0

❯ npm --version
8.3.0

❯ serverless --version
Framework Core: 2.71.0
Plugin: 5.5.3
SDK: 4.3.0
Components: 3.18.1

❯ aws --version
aws-cli/2.4.11 Python/3.9.9 Darwin/20.6.0 source/x86_64 prompt/off

AWSの認証情報をセットアップします。
aws configure コマンドを実行して、プロンプトに従えばOKです。

$ aws configure
AWS Access Key ID [None]: your_access_key_id
AWS Secret Access Key [None]: your_secret_access_key
Default region name [None]:
Default output format [None]:

アクセスキー ID とシークレットアクセスキーがない場合は、
設定の基本 - AWS Command Line Interfaceアクセスキー ID とシークレットアクセスキーのところで作成の手順が記載されていますので、そちらで作成してから登録してください。

参考リンク

プロジェクトの作成

では早速、プロジェクトを作成します。

❯ sls create --template aws-nodejs-typescript --path nodejs-typescript-test
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/**/nodejs-typescript-test"
Serverless: Successfully generated boilerplate for template: "aws-nodejs-typescript"

テンプレートで生成されるコードはこちらになります。

ちょっと気になった点①

テンプレートで作成したREADME.mdによると、ディレクトリの構成は以下のようになっているらしい。

.
├── src
│   ├── functions               # Lambda configuration and source code folder
│   │   ├── hello
│   │   │   ├── handler.ts      # `Hello` lambda source code
│   │   │   ├── index.ts        # `Hello` lambda Serverless configuration
│   │   │   ├── mock.json       # `Hello` lambda input parameter, if any, for local invocation
│   │   │   └── schema.ts       # `Hello` lambda input event JSON-Schema
│   │   │
│   │   └── index.ts            # Import/export of all lambda configurations
│   │
│   └── libs                    # Lambda shared code
│       └── apiGateway.ts       # API Gateway specific helpers
│       └── handlerResolver.ts  # Sharable library for resolving lambda handlers
│       └── lambda.ts           # Lambda middleware
│
├── package.json
├── serverless.ts               # Serverless service file
├── tsconfig.json               # Typescript compiler configuration
├── tsconfig.paths.json         # Typescript paths
└── webpack.config.js           # Webpack configuration

ですが、serverless.tsを確認すると、pluginsにserverless-esbuildとあり、ビルドツールはesbuildを用いてる模様です。そのため、一番下のwebpack.config.jsは生成されません。

releasesから確認すると、v2.60.0で、

Switch to esbuild in aws-nodejs-typescript

とあるので、このバージョンで変更があったようですが、README.mdが変わってないみたいですね。(コミットチャンス?)なので、こちらは一旦気にしなくて大丈夫そうです。

yarn install

以下のように、yarn installを実行して、ライブラリをインストールします。

cd nodejs-typescript-test/

❯ yarn
yarn install v1.22.17
info No lockfile found.
[1/5] 🔍  Validating package.json...
[2/5] 🔍  Resolving packages...
warning serverless > aws-sdk > uuid@3.3.2: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
warning serverless > @serverless/cli > @serverless/utils > uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
warning serverless > aws-sdk > querystring@0.2.0: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
warning serverless > @serverless/platform-client > querystring@0.2.1: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
warning serverless > aws-sdk > url > querystring@0.2.0: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
warning serverless > @serverless/components > @serverless/platform-client-china > querystring@0.2.1: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
warning serverless > json-refs > path-loader > superagent@3.8.3: Please upgrade to v7.0.2+ of superagent.  We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing.  See the releases tab for more information at <https://github.com/visionmedia/superagent/releases>. Thanks to @shadowgate15, @spence-s, and @niftylettuce. Superagent is sponsored by Forward Email at <https://forwardemail.net>.
warning serverless > json-refs > path-loader > superagent > formidable@1.2.6: Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau
warning serverless > @serverless/components > @serverless/platform-client-china > @serverless/utils-china > kafka-node > uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
warning serverless > @serverless/components > @serverless/platform-client-china > @serverless/utils-china > @tencent-sdk/capi > request-promise-native@1.0.9: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142
warning serverless > @serverless/components > @serverless/platform-client-china > @serverless/utils-china > @tencent-sdk/capi > request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
warning serverless > @serverless/components > @serverless/platform-client-china > @serverless/utils-china > @tencent-sdk/capi > request > uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
warning serverless > @serverless/components > @serverless/platform-client-china > @serverless/utils-china > @tencent-sdk/capi > request > har-validator@5.1.5: this library is no longer supported
[3/5] 🚚  Fetching packages...
[4/5] 🔗  Linking dependencies...
warning "serverless > @serverless/components > inquirer-autocomplete-prompt@1.4.0" has unmet peer dependency "inquirer@^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0".
warning " > serverless-esbuild@1.23.3" has unmet peer dependency "esbuild@>=0.8 <0.15".
[5/5] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in 137.15s.

ちょっと気になった点②

serverless.tsは以下のように生成されています。

serverless.ts
import type { AWS } from '@serverless/typescript';

import hello from '@functions/hello';

const serverlessConfiguration: AWS = {
  service: 'nodejs-typescript-test',
  frameworkVersion: '2',
  plugins: ['serverless-esbuild'],
  provider: {
    name: 'aws',
    runtime: 'nodejs14.x',
    apiGateway: {
      minimumCompressionSize: 1024,
      shouldStartNameWithService: true,
    },
    environment: {
      AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
      NODE_OPTIONS: '--enable-source-maps --stack-trace-limit=1000',
    },
    lambdaHashingVersion: '20201221',
  },
  // import the function via paths
  functions: { hello },
  package: { individually: true },
  custom: {
    esbuild: {
      bundle: true,
      minify: false,
      sourcemap: true,
      exclude: ['aws-sdk'],
      target: 'node14',
      define: { 'require.resolve': undefined },
      platform: 'node',
      concurrency: 10,
    },
  },
};

module.exports = serverlessConfiguration;

ここで、ymlからtsに変わって、どう変化しているのかを確認するために、テンプレートaws-nodejsで生成される、serverless.ymlと比較してみましょう。

serverless.yml
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!

service: aws-nodejs # NOTE: update this with your service name
# app and org for use with dashboard.serverless.com
#app: your-app-name
#org: your-org-name

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
frameworkVersion: '2'

provider:
  name: aws
  runtime: nodejs12.x
  lambdaHashingVersion: 20201221

# you can overwrite defaults here
#  stage: dev
#  region: us-east-1

# you can add statements to the Lambda function's IAM Role here
#  iam:
#    role:
#      statements:
#        - Effect: "Allow"
#          Action:
#            - "s3:ListBucket"
#          Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ]  }
#        - Effect: "Allow"
#          Action:
#            - "s3:PutObject"
#          Resource:
#            Fn::Join:
#              - ""
#              - - "arn:aws:s3:::"
#                - "Ref" : "ServerlessDeploymentBucket"
#                - "/*"

# you can define service wide environment variables here
#  environment:
#    variable1: value1

# you can add packaging information here
#package:
#  patterns:
#    - '!exclude-me.js'
#    - '!exclude-me-dir/**'
#    - include-me.js
#    - include-me-dir/**

functions:
  hello:
    handler: handler.hello
#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
#    events:
#      - httpApi:
#          path: /users/create
#          method: get
#      - websocket: $connect
#      - s3: ${env:BUCKET}
#      - schedule: rate(10 minutes)
#      - sns: greeter-topic
#      - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
#      - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
#      - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
#      - iot:
#          sql: "SELECT * FROM 'some_topic'"
#      - cloudwatchEvent:
#          event:
#            source:
#              - "aws.ec2"
#            detail-type:
#              - "EC2 Instance State-change Notification"
#            detail:
#              state:
#                - pending
#      - cloudwatchLog: '/aws/lambda/hello'
#      - cognitoUserPool:
#          pool: MyUserPool
#          trigger: PreSignUp
#      - alb:
#          listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/
#          priority: 1
#          conditions:
#            host: example.com
#            path: /hello

#    Define function environment variables here
#    environment:
#      variable2: value2

# you can add CloudFormation resource templates here
#resources:
#  Resources:
#    NewResource:
#      Type: AWS::S3::Bucket
#      Properties:
#        BucketName: my-new-bucket
#  Outputs:
#     NewOutput:
#       Description: "Description for the output"
#       Value: "Some output value"

概ね、ymlからtsにどんな感じで書き換えたらいいのか、雰囲気は掴めるんじゃないかなと思います。僕が少し気になったのは、ymlでは、

serverless.yml(抜粋)
stage: ${opt:stage, 'dev'}

のように定義していた場所については、tsではどんな感じになるんだろう、と思いました。

Serverless Frameworkの`serverless.yaml`を`serverless.ts`に変換する のように、変換操作を簡単にするために、CLIを作ってくれた方おり、それを用いて変換してみたり、GitHubでみんなserverless.tsどんな感じで実装してるのかなー(https://github.com/search?l=TypeScript&q=%22%24%7Bopt%3A+stage%2C+%27dev%27%7D%22&type=Code) と思って覗いてみましたが、

serverless.ts(抜粋)
stage: '${opt:stage, "dev"}'

みたいな感じで実装していますね。なるほど、という感じです。

functions

ちょうど、serverless.ymlserverless.tsの比較をしていたので、functionsの設定を見てみましょう。typescriptでは、

functions/hello/index.ts
import schema from './schema';
import { handlerPath } from '@libs/handlerResolver';

export default {
  handler: `${handlerPath(__dirname)}/handler.main`,
  events: [
    {
      http: {
        method: 'post',
        path: 'hello',
        request: {
          schemas: {
            'application/json': schema
          }
        }
      }
    }
  ]
}

という風に、serverless.ymlで記載していた設定を、serverless.tsで記載しています。こうやって、責任が分離されているようです。

参考リンク

構成の確認

テンプレートを用いて作成したサービスがどのように挙動するのか確認するために、コードをかいつまんで確認してみましょう。先ほど確認したように、serverless.tsでは

serverless.ts(抜粋)
import hello from '@functions/hello';

functions: { hello },

と定義されているので、helloをみてみます。

└── hello
      ├── handler.ts      # `Hello` lambda source code
      ├── index.ts        # `Hello` lambda Serverless configuration
      ├── mock.json       # `Hello` lambda input parameter, if any, for local invocation
      └── schema.ts       # `Hello` lambda input event JSON-Schema

ではまず、index.tsを再度見てみましょう。

functions/hello/index.ts
import schema from './schema';
import { handlerPath } from '@libs/handlerResolver';

export default {
  handler: `${handlerPath(__dirname)}/handler.main`,
  events: [
    {
      http: {
        method: 'post',
        path: 'hello',
        request: {
          schemas: {
            'application/json': schema
          }
        }
      }
    }
  ]
}

handlerとして、関数が渡されており、eventsには、関数のトリガーとなるイベントが渡されています。

続いて、handler.tsを見てみます。

functions/hello/handler.ts
import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/apiGateway';
import { formatJSONResponse } from '@libs/apiGateway';
import { middyfy } from '@libs/lambda';

import schema from './schema';

const hello: ValidatedEventAPIGatewayProxyEvent<typeof schema> = async (event) => {
  return formatJSONResponse({
    message: `Hello ${event.body.name}, welcome to the exciting Serverless world!`,
    event,
  });
}

export const main = middyfy(hello);

さきほど登録していたように、APIのリクエストがあった際にJSONのレスポンスが返されるように定義されています。

以上から、'hello'というパスに対して、POSTリクエストを送ると、messageとeventのプロパティーを持つ、JSONが返ってくることがわかりました、

では、続いてローカル実行をして、確認してみましょう。

ローカル実行

README.mdに記載があるように、ローカルで実行してみましょう。

❯ yarn sls invoke local -f hello --path src/functions/hello/mock.json
yarn run v1.22.17
$ /Users/**/nodejs-typescript-test/node_modules/.bin/sls invoke local -f hello --path src/functions/hello/mock.json

 Error ---------------------------------------------------

  Error: Cannot find module 'esbuild'
  Require stack:
  - /Users/**/nodejs-typescript-test/node_modules/serverless-esbuild/dist/index.js
  - /Users/**/nodejs-typescript-test/node_modules/serverless/lib/classes/PluginManager.js
  - /Users/**/nodejs-typescript-test/node_modules/serverless/lib/Serverless.js
  - /Users/**/nodejs-typescript-test/node_modules/serverless/scripts/serverless.js
  - /Users/**/nodejs-typescript-test/node_modules/serverless/bin/serverless.js
      at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
      at Function.Module._resolveFilename.sharedData.moduleResolveFilenameHook.installedValue (/Users/**/nodejs-typescript-test/node_modules/@cspotcode/source-map-support/source-map-support.js:679:30)
      at Function.Module._resolveFilename (/Users/**/nodejs-typescript-test/node_modules/tsconfig-paths/lib/register.js:75:40)
      at Function.Module._load (node:internal/modules/cjs/loader:778:27)
      at Module.require (node:internal/modules/cjs/loader:999:19)
      at require (node:internal/modules/cjs/helpers:102:18)
      at Object.<anonymous> (/Users/**/nodejs-typescript-test/node_modules/serverless-esbuild/dist/index.js:13:19)
      at Module._compile (node:internal/modules/cjs/loader:1097:14)
      at Object.Module._extensions..js (node:internal/modules/cjs/loader:1149:10)
      at Module.load (node:internal/modules/cjs/loader:975:32)
      at Function.Module._load (node:internal/modules/cjs/loader:822:12)
      at Module.require (node:internal/modules/cjs/loader:999:19)
      at require (node:internal/modules/cjs/helpers:102:18)
      at PluginManager.requireServicePlugin (/Users/**/nodejs-typescript-test/node_modules/serverless/lib/classes/PluginManager.js:164:14)
      at /Users/**/nodejs-typescript-test/node_modules/serverless/lib/classes/PluginManager.js:186:27
      at Array.map (<anonymous>)
      at PluginManager.resolveServicePlugins (/Users/**/nodejs-typescript-test/node_modules/serverless/lib/classes/PluginManager.js:183:10)
      at PluginManager.loadAllPlugins (/Users/**/nodejs-typescript-test/node_modules/serverless/lib/classes/PluginManager.js:139:10)
      at Serverless.init (/Users/**/nodejs-typescript-test/node_modules/serverless/lib/Serverless.js:213:30)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async /Users/**/nodejs-typescript-test/node_modules/serverless/scripts/serverless.js:589:7

     For debugging logs, run again after setting the "SLS_DEBUG=*" environment variable.

  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Issues:        forum.serverless.com

  Your Environment Information ---------------------------
     Operating System:          darwin
     Node Version:              17.3.1
     Framework Version:         2.71.0 (local)
     Plugin Version:            5.5.3
     SDK Version:               4.3.0
     Components Version:        3.18.1

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

と言われましたので、esbuildを追加します。

❯ yarn add esbuild
yarn add v1.22.17
[1/5] 🔍  Validating package.json...
[2/5] 🔍  Resolving packages...
[3/5] 🚚  Fetching packages...
[4/5] 🔗  Linking dependencies...
warning "serverless > @serverless/components > inquirer-autocomplete-prompt@1.4.0" has unmet peer dependency "inquirer@^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0".
[5/5] 🔨  Building fresh packages...
success Saved lockfile.
success Saved 2 new dependencies.
info Direct dependencies
└─ esbuild@0.14.11
info All dependencies
├─ esbuild-darwin-64@0.14.11
└─ esbuild@0.14.11
✨  Done in 18.79s.

再度、チャレンジします。

❯ yarn sls invoke local -f hello --path src/functions/hello/mock.json
yarn run v1.22.17
$ /Users/**/nodejs-typescript-test/node_modules/.bin/sls invoke local -f hello --path src/functions/hello/mock.json
Serverless: Compiling to node14 bundle with esbuild...
Serverless: Compiling with concurrency: 10
Serverless: Compiling completed.
{
    "statusCode": 200,
    "body": "{\"message\":\"Hello Frederic, welcome to the exciting Serverless world!\",\"event\":{\"headers\":{\"Content-Type\":\"application/json\"},\"body\":{\"name\":\"Frederic\"},\"rawBody\":\"{\\\"name\\\": \\\"Frederic\\\"}\"}}"
}
✨  Done in 6.22s.

正しくレスポンスが返ってきたようなので、ローカル実行はうまくいったようですね。

デプロイ

README.mdに記載があるように、awsにデプロイしてみましょう。

❯ yarn sls deploy
yarn run v1.22.17
$ /Users/**/nodejs-typescript-test/node_modules/.bin/sls deploy
Serverless: Compiling to node14 bundle with esbuild...
Serverless: Compiling with concurrency: 10
Serverless: Compiling completed.
(node:57802) [DEP0147] DeprecationWarning: In future versions of Node.js, fs.rmdir(path, { recursive: true }) will be removed. Use fs.rm(path, { recursive: true }) instead
(Use `node --trace-deprecation ...` to show where the warning was created)
Serverless: Zip function: hello - 13.66 KB [36 ms]
Serverless: Packaging service...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
........
Serverless: Stack create finished...
Serverless: Ensuring that deployment bucket exists
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service hello.zip file to S3 (13.99 kB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
....................................
Serverless: Stack update finished...
Service Information
service: nodejs-typescript-test
stage: dev
region: us-east-1
stack: nodejs-typescript-test-dev
resources: 13
api keys:
  None
endpoints:
  POST - https://myApiEndpoint/dev/hello
functions:
  hello: nodejs-typescript-test-dev-hello
layers:
  None

Monitor APIs by route with the Serverless Dashboard: run "serverless"
✨  Done in 121.06s.

デプロイが完了したようです。aws consoleを開いて確認してみましょう。

スクリーンショット 2022-01-16 23.27.15.png

無事にLambdaがデプロイされているのを確認できました。さらにコンソールから、コンパイルされてミニファイ化されたコードを確認することができます。

スクリーンショット 2022-01-16 23.29.49.png

デプロイができたので、README.mdに記載されているように、挙動を確認してみましょう。

❯ curl --location --request POST 'https://myApiEndpoint/dev/hello' \
      --header 'Content-Type: application/json' \
      --data-raw '{
      "name": "Frederic"
  }'
{"message":"Hello Frederic, welcome to the exciting Serverless world!","event":{"resource":"/hello","path":"/hello","httpMethod":"POST","headers":{"Accept":"*/*","CloudFront-Forwarded-Proto":"https",...

デプロイ後にローカルのディレクトリ内に、.serverlessというディレクトリを確認することができます。
その中にCloudFormatiomのjsonと、Lambda関数のzipが格納されています。

スクリーンショット 2022-01-17 0.36.25.png

また、デプロイ後、S3を確認すると、nodejs-typescript-test-d-serverlessdeploymentbuck-**のようなバケットが作成されていることを確認することができます。

スクリーンショット 2022-01-17 0.41.09.png

中身を確認すると、compiled-cloudformation-template.jsonhello.zipの2つが格納されています。hello.zipは、コンパイルされてミニファイ化されたコードが圧縮されています。

スクリーンショット 2022-01-16 23.43.18.png

プロジェクトの削除

デプロイ済みのサービスを削除します。

❯ yarn sls remove
yarn run v1.22.17
$ /Users/**/nodejs-typescript-test/node_modules/.bin/sls remove
Serverless: Getting all objects in S3 bucket...
Serverless: Removing objects in S3 bucket...
Serverless: Removing Stack...
Serverless: Checking Stack delete progress...
........................
Serverless: Stack delete finished...

Serverless: Stack delete finished...
✨  Done in 50.42s.

これにより、AWS上のリソースが削除されることが確認できます。

まとめ

今回、ServerlessFrameworkでaws-nodejs-typescriptのテンプレートを用いて、一通り挙動を確認しました。テンプレートとは言ったものの、ある程度挙動が確認できるように、すでに実装されている部分があり、一つ一つ読み解く必要がありました。通常のテンプレートとは違うので戸惑うかもしれませんが、そういった方の助けになれば幸いです。

全体を通しての参考リンク

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?