apeescape2.com
  • メイン
  • リモートの台頭
  • 投資家と資金調達
  • 製品ライフサイクル
  • エンジニアリング管理
技術

Node.jsでの安全なRESTAPIの作成

アプリケーションプログラミングインターフェイス(API)はいたるところにあります。これらは、ソフトウェアが他のソフトウェア(内部または外部)と一貫して通信できるようにします。これは、再利用性は言うまでもなく、スケーラビリティの重要な要素です。

今日、オンラインサービスが公開APIを使用することは非常に一般的です。これらにより、他の開発者はソーシャルメディアログイン、クレジットカード支払い、行動追跡などの機能を簡単に統合できます。ザ・ デファクト これに使用する標準は、REpresentational State Transfer(REST)と呼ばれます。

多数のプラットフォームとプログラミング言語をタスクに使用できますが、たとえば、 ASP.NETCore 、 Laravel(PHP) 、または ボトル(Python) —このチュートリアルでは、次のスタックを使用して、基本的で安全なRESTAPIバックエンドを構築します。



  • Node.js、読者はすでにある程度の知識があるはずです
  • Expressは、Node.jsでの一般的なWebサーバータスクの構築を大幅に簡素化し、RESTAPIバックエンドを構築する際の標準的な料金です。
  • Mongoose。バックエンドをMongoDBデータベースに接続します

このチュートリアルに従う開発者は、ターミナル(またはコマンドプロンプト)にも慣れている必要があります。

注:ここではフロントエンドのコードベースについては説明しませんが、バックエンドがJavaScriptで記述されているため、スタック全体でコード(オブジェクトモデルなど)を共有すると便利です。

RESTAPIの構造

REST APIは、ステートレス操作の一般的なセットを使用してデータにアクセスして操作するために使用されます。これらの操作はHTTPプロトコルに不可欠であり、重要な作成、読み取り、更新、および削除(CRUD)機能を表しますが、クリーンな1対1の方法ではありません。

  • POST (リソースを作成するか、通常はデータを提供します)
  • GET (リソースまたは個々のリソースのインデックスを取得します)
  • PUT (リソースの作成または置換)
  • PATCH (リソースの更新/変更)
  • DELETE (リソースを削除します)

これらのHTTP操作とリソース名をアドレスとして使用して、操作ごとにエンドポイントを作成することでRESTAPIを構築できます。また、パターンを実装することで、安定したわかりやすい基盤が得られ、コードを迅速に進化させ、後で維持することができます。前述のように、サードパーティの機能を統合するために同じ基盤が使用され、そのほとんどは同様にREST APIを使用して、そのような統合をより高速にします。

とりあえず、Node.jsを使用して安全なRESTAPIの作成を始めましょう。

このチュートリアルでは、usersというリソース用の非常に一般的な(そして非常に実用的な)RESTAPIを作成します。

リソースの基本構造は次のとおりです。

  • id (自動生成されたUUID)
  • firstName
  • lastName
  • email
  • password
  • permissionLevel (このユーザーは何をすることができますか?)

そして、そのリソースに対して次の操作を作成します。

  • POSTエンドポイントで/users (新しいユーザーを作成します)
  • GETエンドポイントで/users (すべてのユーザーを一覧表示)
  • GETエンドポイントで/users/:userId (特定のユーザーを取得します)
  • PATCHエンドポイントで/users/:userId (特定のユーザーのデータを更新します)
  • DELETEエンドポイントで/users/:userId (特定のユーザーを削除します)

また、アクセストークンにはJSON Webトークン(JWT)を使用します。そのために、authという別のリソースを作成します。これは、ユーザーの電子メールとパスワードを期待し、その見返りとして、特定の操作での認証に使用されるトークンを生成します。 (DejanMilosevicのすばらしい記事 Javaでの安全なRESTアプリケーションのためのJWT これについてさらに詳しく説明します。原則は同じです。)

RESTAPIチュートリアルのセットアップ

まず、最新のNode.jsバージョンがインストールされていることを確認してください。この記事では、バージョン14.9.0を使用します。古いバージョンでも動作する可能性があります。

次に、あなたが持っていることを確認してください MongoDB インストールされています。ここで使用するMongooseとMongoDBの詳細については説明しませんが、基本を実行するには、サーバーをサービスとしてではなく、インタラクティブモードで(つまり、コマンドラインからmongoとして)起動するだけです。これは、このチュートリアルのある時点で、Node.jsコードを介してではなく、MongoDBと直接対話する必要があるためです。

注:MongoDBでは、一部のRDBMSシナリオのように特定のデータベースを作成する必要はありません。 Node.jsコードからの最初の挿入呼び出しは、その作成を自動的にトリガーします。

このチュートリアルには、作業プロジェクトに必要なすべてのコードが含まれているわけではありません。代わりに、クローンを作成することを目的としています コンパニオンレポ 読み進めながらハイライトをたどるだけですが、必要に応じて、必要に応じてリポジトリから特定のファイルやスニペットをコピーすることもできます。

結果のrest-api-tutorial/に移動しますターミナルのフォルダ。プロジェクトに3つのモジュールフォルダが含まれていることがわかります。

  • common (すべての共有サービス、およびユーザーモジュール間で共有される情報の処理)
  • users (ユーザーに関するすべて)
  • auth (JWT生成とログインフローの処理)

ここで、npm installを実行します(または、お持ちの場合はyarn)

おめでとうございます。これで、単純なRESTAPIバックエンドを実行するために必要なすべての依存関係とセットアップが完了しました。

ユーザーモジュールの作成

使用します マングース 、MongoDBのオブジェクトデータモデリング(ODM)ライブラリで、ユーザースキーマ内にユーザーモデルを作成します。

まず、/users/models/users.model.jsでMongooseスキーマを作成する必要があります。

const userSchema = new Schema({ firstName: String, lastName: String, email: String, password: String, permissionLevel: Number });

スキーマを定義すると、スキーマをユーザーモデルに簡単にアタッチできます。

const userModel = mongoose.model('Users', userSchema);

その後、このモデルを使用して、Expressエンドポイント内で必要なすべてのCRUD操作を実装できます。

お金でクレジットカードを漏らした

users/routes.config.jsでルートを定義することにより、「ユーザーの作成」操作から始めましょう。

app.post('/users', [ UsersController.insert ]);

これは、メインのExpressアプリに取り込まれますindex.jsファイル。 UsersControllerオブジェクトはコントローラーからインポートされ、/users/controllers/users.controller.jsで定義されているパスワードを適切にハッシュします。

exports.insert = (req, res) => { let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512',salt) .update(req.body.password) .digest('base64'); req.body.password = salt + '$' + hash; req.body.permissionLevel = 1; UserModel.createUser(req.body) .then((result) => { res.status(201).send({id: result._id}); }); };

この時点で、サーバー(npm start)を実行し、POSTを送信することで、Mongooseモデルをテストできます。 /usersへのリクエストいくつかのJSONデータを使用:

{ 'firstName' : 'Marcos', 'lastName' : 'Silva', 'email' : ' [email protected] ', 'password' : 's3cr3tp4sswo4rd' }

これに使用できるツールがいくつかあります。不眠症(以下で説明)とPostmanは人気のあるGUIツールであり、curl一般的なCLIの選択です。たとえば、ブラウザの組み込みの開発ツールコンソールからJavaScriptを使用することもできます。

fetch('http://localhost:3600/users', { method: 'POST', headers: { 'Content-type': 'application/json' }, body: JSON.stringify({ 'firstName': 'Marcos', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 's3cr3tp4sswo4rd' }) }) .then(function(response) { return response.json(); }) .then(function(data) { console.log('Request succeeded with JSON response', data); }) .catch(function(error) { console.log('Request failed', error); });

この時点で、有効な投稿の結果は、作成されたユーザーからのIDのみになります:{ 'id': '5b02c5c84817bf28049e58a3' }。 createUserも追加する必要がありますusers/models/users.model.jsのモデルへのメソッド:

exports.createUser = (userData) => { const user = new User(userData); return user.save(); };

これで、ユーザーが存在するかどうかを確認する必要があります。そのために、次のエンドポイントに「IDでユーザーを取得」機能を実装します:users/:userId。

まず、/users/routes/config.jsにルートを作成します。

app.get('/users/:userId', [ UsersController.getById ]);

次に、/users/controllers/users.controller.jsにコントローラーを作成します。

exports.getById = (req, res) => { UserModel.findById(req.params.userId).then((result) => { res.status(200).send(result); }); };

そして最後に、findByIdを追加します/users/models/users.model.jsのモデルへのメソッド:

exports.findById = (id) => { return User.findById(id).then((result) => { result = result.toJSON(); delete result._id; delete result.__v; return result; }); };

応答は次のようになります。

{ 'firstName': 'Marcos', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'Y+XZEaR7J8xAQCc37nf1rw==$p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh+CUQ4n/E0z48mp8SDTpX2ivuQ==', 'permissionLevel': 1, 'id': '5b02c5c84817bf28049e58a3' }

ハッシュ化されたパスワードが表示されることに注意してください。このチュートリアルでは、パスワードを表示していますが、ハッシュ化されている場合でも、パスワードを公開しないことをお勧めします。もう1つ確認できるのは、permissionLevelです。これは、後でユーザー権限を処理するために使用します。

上記のパターンを繰り返して、ユーザーを更新する機能を追加できるようになりました。 PATCHを使用します変更したいフィールドのみを送信できるようになるためです。したがって、ルートはPATCHになります。 /users/:useridに送信すると、変更するフィールドが送信されます。変更は問題のユーザーまたは管理者に制限する必要があり、管理者のみがpermissionLevelを変更できる必要があるため、追加の検証も実装する必要があります。とりあえずそれをスキップして、authモジュールを実装したらそれに戻ります。今のところ、コントローラーは次のようになります。

exports.patchById = (req, res) => { if (req.body.password){ let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest('base64'); req.body.password = salt + '$' + hash; } UserModel.patchUser(req.params.userId, req.body).then((result) => { res.status(204).send({}); }); };

デフォルトでは、リクエストが成功したことを示す応答本文のないHTTPコード204を送信します。

そして、patchUserを追加する必要がありますモデルへの方法:

exports.patchUser = (id, userData) => { return User.findOneAndUpdate({ _id: id }, userData); };

ユーザーリストはGETとして実装されます/users/で次のコントローラーによって:

exports.list = (req, res) => { let limit = req.query.limit && req.query.limit { res.status(200).send(result); }) };

対応するモデルメソッドは次のようになります。

exports.list = (perPage, page) => { return new Promise((resolve, reject) => { User.find() .limit(perPage) .skip(perPage * page) .exec(function (err, users) { if (err) { reject(err); } else { resolve(users); } }) }); };

結果のリスト応答は、次の構造になります。

[ { 'firstName': 'Marco', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'z4tS/DtiH+0Gb4J6QN1K3w==$al6sGxKBKqxRQkDmhnhQpEB6+DQgDRH2qr47BZcqLm4/fphZ7+a9U+HhxsNaSnGB2l05Oem/BLIOkbtOuw1tXA==', 'permissionLevel': 1, 'id': '5b02c5c84817bf28049e58a3' }, { 'firstName': 'Paulo', 'lastName': 'Silva', 'email': ' [email protected] ', 'password': 'wTsqO1kHuVisfDIcgl5YmQ==$cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw==', 'permissionLevel': 1, 'id': '5b02d038b653603d1ca69729' } ]

そして、実装される最後の部分はDELETEです。 /users/:userIdで。

削除するコントローラーは次のとおりです。

exports.removeById = (req, res) => { UserModel.removeById(req.params.userId) .then((result)=>{ res.status(204).send({}); }); };

以前と同じように、コントローラーは確認としてHTTPコード204を返し、コンテンツ本文は返しません。

対応するモデルメソッドは次のようになります。

exports.removeById = (userId) => { return new Promise((resolve, reject) => { User.deleteMany({_id: userId}, (err) => { if (err) { reject(err); } else { resolve(err); } }); }); };

これで、ユーザーリソースを操作するために必要なすべての操作が完了し、ユーザーコントローラーが完成しました。このコードの主なアイデアは、RESTパターンを使用するためのコアコンセプトを提供することです。いくつかの検証と権限を実装するには、このコードに戻る必要がありますが、最初に、セキュリティの構築を開始する必要があります。 authモジュールを作成しましょう。

認証モジュールの作成

usersを保護する前にパーミッションと検証ミドルウェアを実装することでモジュールを作成するには、現在のユーザーの有効なトークンを生成できる必要があります。ユーザーが有効な電子メールとパスワードを提供したことに応じて、JWTを生成します。 JWTは注目に値するJSONWebトークンであり、ユーザーが繰り返し検証することなく、複数のリクエストを安全に実行できるようにするために使用できます。通常、有効期限があり、通信を安全に保つために、新しいトークンが数分ごとに再作成されます。ただし、このチュートリアルでは、トークンの更新を省略し、ログインごとに1つのトークンを使用してトークンをシンプルに保ちます。

まず、POSTのエンドポイントを作成します/authへのリクエスト資源。リクエストの本文には、ユーザーのメールアドレスとパスワードが含まれます。

{ 'email' : ' [email protected] ', 'password' : 's3cr3tp4sswo4rd2' }

コントローラを使用する前に、/authorization/middlewares/verify.user.middleware.jsでユーザーを検証する必要があります。

exports.isPasswordAndUserMatch = (req, res, next) => { UserModel.findByEmail(req.body.email) .then((user)=>{ if(!user[0]){ res.status(404).send({}); }else{ let passwordFields = user[0].password.split('$'); let salt = passwordFields[0]; let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest('base64'); if (hash === passwordFields[1]) { req.body = { userId: user[0]._id, email: user[0].email, permissionLevel: user[0].permissionLevel, provider: 'email', name: user[0].firstName + ' ' + user[0].lastName, }; return next(); } else { return res.status(400).send({errors: ['Invalid email or password']}); } } }); };

これが完了したら、コントローラーに移動してJWTを生成できます。

exports.login = (req, res) => { try { let refreshId = req.body.userId + jwtSecret; let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(refreshId).digest('base64'); req.body.refreshKey = salt; let token = jwt.sign(req.body, jwtSecret); let b = Buffer.from(hash); let refresh_token = b.toString('base64'); res.status(201).send({accessToken: token, refreshToken: refresh_token}); } catch (err) { res.status(500).send({errors: err}); } };

このチュートリアルではトークンを更新しませんが、コントローラーは、そのような生成を有効にして、後続の開発での実装を容易にするように設定されています。

ここで必要なのは、ルートを作成し、/authorization/routes.config.jsで適切なミドルウェアを呼び出すことだけです。

app.post('/auth', [ VerifyUserMiddleware.hasAuthValidFields, VerifyUserMiddleware.isPasswordAndUserMatch, AuthorizationController.login ]);

応答には、生成されたJWTがaccessTokenフィールドに含まれます。

{ 'accessToken': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmNg-i44VQlUEWP3YIAYXVO-74803v1mu-y9QPUQ5VY', 'refreshToken': 'U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ==' }

トークンを作成したら、Authorization内で使用できます。 Bearer ACCESS_TOKENの形式を使用したヘッダー。

権限と検証ミドルウェアの作成

最初に定義する必要があるのは、誰がusersを使用できるかです。資源。処理する必要があるシナリオは次のとおりです。

  • ユーザーを作成するためのパブリック(登録プロセス)。このシナリオではJWTを使用しません。
  • ログインしたユーザーと管理者がそのユーザーを更新するためのプライベート。
  • ユーザーアカウントを削除するための管理者専用のプライベート。

これらのシナリオを特定したら、最初に、ユーザーが有効なJWTを使用しているかどうかを常に検証するミドルウェアが必要になります。 /common/middlewares/auth.validation.middleware.jsのミドルウェア次のように単純にすることができます。

exports.validJWTNeeded = (req, res, next) => { if (req.headers['authorization']) { try { let authorization = req.headers['authorization'].split(' '); if (authorization[0] !== 'Bearer') { return res.status(401).send(); } else { req.jwt = jwt.verify(authorization[1], secret); return next(); } } catch (err) { return res.status(403).send(); } } else { return res.status(401).send(); } };

リクエストエラーの処理にはHTTPエラーコードを使用します。

  • 無効なリクエストのHTTP401
  • 無効なトークンを持つ有効なリクエスト、または無効な権限を持つ有効なトークンのHTTP 403

ビットごとのAND演算子(ビットマスキング)を使用して、アクセス許可を制御できます。必要な各パーミッションを2の累乗として設定すると、32ビット整数の各ビットを1つのパーミッションとして扱うことができます。管理者は、権限値を2147483647に設定することにより、すべての権限を持つことができます。そのユーザーは、任意のルートにアクセスできます。別の例として、アクセス許可の値が7に設定されているユーザーは、値1、2、および4(2の0、1、および2の累乗)のビットでマークされたロールへのアクセス許可を持ちます。

そのためのミドルウェアは次のようになります。

exports.minimumPermissionLevelRequired = (required_permission_level) => { return (req, res, next) => { let user_permission_level = parseInt(req.jwt.permission_level); let user_id = req.jwt.user_id; if (user_permission_level & required_permission_level) { return next(); } else { return res.status(403).send(); } }; };

ミドルウェアは汎用です。ユーザーのアクセス許可レベルと必要なアクセス許可レベルが少なくとも1ビットで一致する場合、結果はゼロより大きくなり、アクションを続行できます。それ以外の場合は、HTTPコード403が返されます。

次に、認証ミドルウェアを/users/routes.config.jsのユーザーのモジュールルートに追加する必要があります。

app.post('/users', [ UsersController.insert ]); app.get('/users', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(PAID), UsersController.list ]); app.get('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.getById ]); app.patch('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.patchById ]); app.delete('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(ADMIN), UsersController.removeById ]);

これで、RESTAPIの基本的な開発は終わりです。やらなければならないことは、それをすべてテストすることだけです。

不眠症の実行とテスト

不眠症 良い無料バージョンのまともなRESTクライアントです。もちろん、ベストプラクティスは、コードテストを含め、プロジェクトに適切なエラーレポートを実装することですが、サードパーティのRESTクライアントは、エラーレポートとサービスのデバッグが利用できない場合に、サードパーティのソリューションをテストおよび実装するのに最適です。ここでは、これを使用してアプリケーションの役割を果たし、APIで何が起こっているかについての洞察を得ます。

ユーザーを作成するには、POSTを実行する必要があります。必要なフィールドを適切なエンドポイントに送信し、生成されたIDを後で使用できるように保存します。

ユーザーを作成するための適切なデータを要求する

APIはユーザーIDで応答します:

userIDによる確認応答

これで、/auth/を使用してJWTを生成できます。終点:

ログインデータを使用したリクエスト

応答としてトークンを取得する必要があります。

近接のゲシュタルト法則の例

対応するJSONWebトークンを含む確認

accessTokenを取得し、接頭辞Bearer を付けます(スペースを覚えておいてください)、それをAuthorizationの下のリクエストヘッダーに追加します。

転送するヘッダーの設定には、認証JWTが含まれます

パーミッションミドルウェアを実装したのでこれを行わないと、登録以外のすべてのリクエストはHTTPコード401を返します。ただし、有効なトークンが設定されていると、/users/:userIdから次の応答が返されます。

示されたユーザーのデータをリストする応答

また、前述のとおり、教育目的およびわかりやすくするために、すべてのフィールドを表示しています。パスワード(ハッシュまたはその他)は、応答に表示されないようにする必要があります。

ユーザーのリストを取得してみましょう。

すべてのユーザーのリストをリクエストする

驚き! 403応答を受け取ります。

適切な許可レベルがないため、アクションが拒否されました

ユーザーには、このエンドポイントにアクセスするための権限がありません。 permissionLevelを変更する必要がありますユーザーの1から7まで(または、無料と有料のアクセス許可レベルがそれぞれ1と4として表されるため、5でもかまいません)。これは、MongoDBのインタラクティブなプロンプトで、次のように手動で行うことができます(IDを使用)ローカル結果に変更):

db.users.update({'_id' : ObjectId('5b02c5c84817bf28049e58a3')},{$set:{'permissionLevel':5}})

次に、新しいJWTを生成する必要があります。

それが行われた後、適切な応答が得られます。

すべてのユーザーとそのデータによる応答

次に、PATCHを送信して更新機能をテストしましょう。 /users/:userIdにいくつかのフィールドを指定してリクエストします終点:

更新する部分データを含むリクエスト

操作が成功したことの確認として204の応答が期待されますが、ユーザーにもう一度確認を要求することができます。

変更が成功した後の応答

最後に、ユーザーを削除する必要があります。上記のように新しいユーザーを作成し(ユーザーIDを忘れないでください)、管理者ユーザーに適切なJWTがあることを確認する必要があります。新しいユーザーが削除操作を実行できるようにするには、権限を2053(2048— ADMIN —および以前の5)に設定する必要があります。これが完了し、新しいJWTが生成されたら、Authorizationを更新する必要がありますリクエストヘッダー:

ユーザーを削除するためのセットアップをリクエストする

DELETEを送信する/users/:userIdにリクエストすると、確認として204の応答が返されます。繰り返しになりますが、/users/をリクエストして確認できます既存のすべてのユーザーを一覧表示します。

RESTAPIの次のステップ

このチュートリアルで説明するツールとメソッドを使用すると、Node.jsでシンプルで安全なRESTAPIを作成できるようになります。プロセスに不可欠ではない多くのベストプラクティスがスキップされたため、次のことを忘れないでください。

  • 適切な検証を実装します(たとえば、ユーザーの電子メールが一意であることを確認します)
  • ユニットテストとエラーレポートを実装する
  • ユーザーが自分の権限レベルを変更できないようにする
  • 管理者が自分自身を削除できないようにする
  • 機密情報(ハッシュ化されたパスワードなど)の開示を防止する
  • JWTシークレットをcommon/config/env.config.jsから移動しますオフレポ、非環境ベース 秘密の配布メカニズム

読者にとっての最後の演習の1つは、コードベースをJavaScriptPromiseの使用から 非同期/待機 技術。

興味があるかもしれないあなたのために、今もあります TypeScriptバージョン 利用可能なプロジェクトの。

関連: REST仕様でこれまでに行ったことのない5つのこと

最適なフロントエンドフレームワークを選択する方法

Webフロントエンド

最適なフロントエンドフレームワークを選択する方法
ゲシュタルトの設計原則を探る

ゲシュタルトの設計原則を探る

Uxデザイン

人気の投稿
ApeeScapeは、自動車業界が新興のモビリティ業界に移行する際のソフトウェア開発者のニーズを満たします
ApeeScapeは、自動車業界が新興のモビリティ業界に移行する際のソフトウェア開発者のニーズを満たします
フラックスとバックボーンを使用したReactアプリの単純なデータフロー:例を含むチュートリアル
フラックスとバックボーンを使用したReactアプリの単純なデータフロー:例を含むチュートリアル
Adobe XD vs Sketch – Showdown 2020
Adobe XD vs Sketch – Showdown 2020
欧州連合でビジネスを行う
欧州連合でビジネスを行う
閉じられないAndroidPOSアプリの構築
閉じられないAndroidPOSアプリの構築
 
混合整数計画法:計算による意思決定のガイド
混合整数計画法:計算による意思決定のガイド
開発者向けの設計ワークフロー:より優れたUX / UIを時間と形式で提供
開発者向けの設計ワークフロー:より優れたUX / UIを時間と形式で提供
進化する絵文字:メッセージングの新しい顔のためのデザイン
進化する絵文字:メッセージングの新しい顔のためのデザイン
50の最高のスケッチプラグインの究極のリスト
50の最高のスケッチプラグインの究極のリスト
ブランドイラスト101:ナラティブの視覚化
ブランドイラスト101:ナラティブの視覚化
人気の投稿
  • ノードjsのパフォーマンスとJava
  • Webデザインのブートストラップとは
  • 一般的な運命のゲシュタルト原理
  • 拡張現実と複合現実
  • アジャイルスクラムとかんばんの違い
カテゴリー
製品の担当者とチーム 設計プロセス ライフスタイル アジャイルタレント Kpiと分析 分散チーム アジャイル データサイエンスとデータベース デザイナーライフ 財務プロセス

© 2021 | 全著作権所有

apeescape2.com