Jekyll
2020-07-16T08:25:08-05:00
https://mherman.org//
Michael Herman
Michael Herman
Pulling Images From Private Docker Registries on GitLab CI
2020-07-16T00:00:00-05:00
2020-07-16T00:00:00-05:00
https://mherman.org/blog/gitlab-ci-private-docker-registry
<p>Want to use an image from a private Docker registry as the base for GitLab Runner’s Docker executor?</p>
<p><a href="https://aws.amazon.com/ecr/">ECR</a> example:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s"><AWS_ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/<NAMESPACE>:<TAG></span>
</code></pre></div></div>
<p>Full job:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">test:api:dev</span><span class="pi">:</span>
<span class="na">stage</span><span class="pi">:</span> <span class="s">test</span>
<span class="na">image</span><span class="pi">:</span> <span class="s"><AWS_ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com/<NAMESPACE>:<TAG></span>
<span class="na">services</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">postgres:latest</span>
<span class="pi">-</span> <span class="s">redis:latest</span>
<span class="na">variables</span><span class="pi">:</span>
<span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">data_api</span>
<span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">runner</span>
<span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">runner</span>
<span class="na">DATABASE_URL</span><span class="pi">:</span> <span class="s">postgres://runner:runner@postgres:5432/data_api</span>
<span class="na">script</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">cd api</span>
<span class="pi">-</span> <span class="s">export DEBUG=1</span>
<span class="pi">-</span> <span class="s">export ENVIRONMENT=dev</span>
<span class="pi">-</span> <span class="s">export CELERY_BROKER_URL=redis://redis</span>
<span class="pi">-</span> <span class="s">export CELERY_RESULT_BACKEND=redis://redis</span>
<span class="pi">-</span> <span class="s">python -m pytest -p no:warnings .</span>
<span class="pi">-</span> <span class="s">flake8 .</span>
<span class="pi">-</span> <span class="s">black --exclude="migrations|env" --check .</span>
<span class="pi">-</span> <span class="s">isort --skip=migrations --skip=env --check-only</span>
<span class="pi">-</span> <span class="s">export DEBUG=0</span>
<span class="pi">-</span> <span class="s">export ENVIRONMENT=prod</span>
<span class="pi">-</span> <span class="s">python manage.py check --deploy --fail-level=WARNING</span>
</code></pre></div></div>
<p>Assuming the image exists on the registry, you can set the <code class="highlighter-rouge">DOCKER_AUTH_CONFIG</code> variable within your project’s Settings > CI/CD page:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"auths"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"registry.example.com:5000"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"auth"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TBD"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>The value of <code class="highlighter-rouge">auth</code> is a base64-encoded version of your username and password that you use to authenticate into the registry:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">echo</span> <span class="nt">-n</span> <span class="s2">"my_username:my_password"</span> | base64
</code></pre></div></div>
<p>Continuing with the ECR example, you can generate a password using the following command:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">--rm</span> <span class="se">\</span>
<span class="nt">-e</span> <span class="nv">AWS_ACCESS_KEY_ID</span><span class="o">=</span><AWS_ACCESS_KEY_ID> <span class="se">\</span>
<span class="nt">-e</span> <span class="nv">AWS_SECRET_ACCESS_KEY</span><span class="o">=</span><AWS_SECRET_ACCESS_KEY> <span class="se">\</span>
amazon/aws-cli ecr get-login-password <span class="se">\</span>
<span class="nt">--region</span> <AWS_REGION>
</code></pre></div></div>
<p>To test, run:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker login <span class="nt">-u</span> AWS <span class="nt">-p</span> <GENERATED_PASSWORD> <AWS_ACCOUNT_ID>.dkr.ecr.<AWS_REGION>.amazonaws.com
Login Succeeded
</code></pre></div></div>
<p>Now, add the <code class="highlighter-rouge">DOCKER_AUTH_CONFIG</code> variable to your project’s Settings > CI/CD page:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"auths"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"registry.example.com:5000"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"auth"</span><span class="p">:</span><span class="w"> </span><span class="s2">"<GENERATED_PASSWORD>"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Test out your build. You should see something similar to the following in your logs, indicating that the login was successful:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Authenticating with credentials from <span class="nv">$DOCKER_AUTH_CONFIG</span>
Pulling docker image <span class="o">[</span>MASKED].dkr.ecr.us-east-1.amazonaws.com/api:latest ...
</code></pre></div></div>
<p>Unfortunately, we’re not done yet since the generated password/token from the <a href="https://docs.aws.amazon.com/cli/latest/reference/ecr/get-login-password.html">get-login-password</a> command is only valid for 12 hours. So, we need to dynamically update the <code class="highlighter-rouge">DOCKER_AUTH_CONFIG</code> variable with a new password. We can set up a new job for this:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">build:aws_auth</span><span class="pi">:</span>
<span class="na">stage</span><span class="pi">:</span> <span class="s">build</span>
<span class="na">services</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">docker:dind</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">docker:stable</span>
<span class="na">variables</span><span class="pi">:</span>
<span class="na">DOCKER_DRIVER</span><span class="pi">:</span> <span class="s">overlay2</span>
<span class="na">DOCKER_BUILDKIT</span><span class="pi">:</span> <span class="s">1</span>
<span class="na">script</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID</span>
<span class="pi">-</span> <span class="s">export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY</span>
<span class="pi">-</span> <span class="s">export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY</span>
<span class="pi">-</span> <span class="s">export AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION</span>
<span class="pi">-</span> <span class="s">export TOKEN=$TOKEN</span>
<span class="pi">-</span> <span class="s">export PROJECT_ID=$PROJECT_ID</span>
<span class="pi">-</span> <span class="s">apk add --no-cache curl jq bash</span>
<span class="pi">-</span> <span class="s">chmod +x ./aws_auth.sh</span>
<span class="pi">-</span> <span class="s">bash ./aws_auth.sh</span>
</code></pre></div></div>
<p>Here, after exporting the appropriate environment variables (so we can access them in the <em>aws_auth.sh</em> script), we installed the appropriate dependencies, and then ran the <em>aws_auth.sh</em> script.</p>
<p><em>aws_auth.sh</em>:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>
<span class="nb">set</span> <span class="nt">-e</span>
<span class="nv">AWS_PASSWORD</span><span class="o">=</span><span class="k">$(</span>docker run <span class="nt">--rm</span> <span class="se">\</span>
<span class="nt">-e</span> <span class="nv">AWS_ACCESS_KEY_ID</span><span class="o">=</span><span class="nv">$AWS_ACCESS_KEY_ID</span> <span class="se">\</span>
<span class="nt">-e</span> <span class="nv">AWS_SECRET_ACCESS_KEY</span><span class="o">=</span><span class="nv">$AWS_SECRET_ACCESS_KEY</span> <span class="se">\</span>
amazon/aws-cli ecr get-login-password <span class="se">\</span>
<span class="nt">--region</span> <span class="nv">$AWS_DEFAULT_REGION</span><span class="k">)</span>
<span class="nv">ENCODED</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="nt">-n</span> <span class="s2">"AWS:</span><span class="nv">$AWS_PASSWORD</span><span class="s2">"</span> | base64<span class="k">)</span>
<span class="nv">PAYLOAD</span><span class="o">=</span><span class="k">$(</span> jq <span class="nt">-n</span> <span class="nt">--arg</span> userpass <span class="s2">"</span><span class="nv">$ENCODED</span><span class="s2">"</span> <span class="s1">'{"auths": {"263993132376.dkr.ecr.us-east-1.amazonaws.com": {"auth": $userpass}}}'</span> <span class="k">)</span>
curl <span class="nt">--request</span> PUT <span class="nt">--header</span> <span class="s2">"PRIVATE-TOKEN:</span><span class="nv">$TOKEN</span><span class="s2">"</span> <span class="s2">"https://gitlab.com/api/v4/projects/</span><span class="nv">$PROJECT_ID</span><span class="s2">/variables/DOCKER_AUTH_CONFIG"</span> <span class="nt">--form</span> <span class="s2">"value=</span><span class="nv">$PAYLOAD</span><span class="s2">"</span>
</code></pre></div></div>
<p>What’s happening?</p>
<ol>
<li>We generated a new password from the <code class="highlighter-rouge">get-login-password</code> command and assigned it to <code class="highlighter-rouge">AWS_PASSWORD</code></li>
<li>We then base64 encoded the username and password and assigned it to <code class="highlighter-rouge">ENCODED</code></li>
<li>We used jq to create the necessary JSON for the value of the <code class="highlighter-rouge">DOCKER_AUTH_CONFIG</code> variable</li>
<li>Finally, using a GitLab <a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">Personal access token</a> we updated the <code class="highlighter-rouge">DOCKER_AUTH_CONFIG</code> variable</li>
</ol>
<p>Make sure to add all variables you project’s Settings > CI/CD page.</p>
<p>Now, the <code class="highlighter-rouge">DOCKER_AUTH_CONFIG</code> variable should be updated with a new password for each build.</p>
<p>That’s it!</p>
<p>–</p>
<p>Helpful Resources:</p>
<ol>
<li>GitLab Runner Issue Thread - <a href="https://gitlab.com/gitlab-org/gitlab-runner/-/issues/1583">Pull images from aws ecr or private registry</a></li>
<li>GitLab Docs - <a href="https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#define-an-image-from-a-private-container-registry">Define an image from a private Container Registry</a></li>
</ol>
Michael Herman
Want to use an image from a private Docker registry as the base for GitLab Runner’s Docker executor?
Dockerizing a React App
2020-04-07T00:00:00-05:00
2020-04-07T00:00:00-05:00
https://mherman.org/blog/dockerizing-a-react-app
<p><a href="https://www.docker.com/">Docker</a> is a containerization tool used to speed up the development and deployment processes. If you’re working with microservices, Docker makes it much easier to link together small, independent services. It also helps to eliminate environment-specific bugs since you can replicate your production environment locally.</p>
<p>This tutorial demonstrates how to Dockerize a React app using the <a href="https://facebook.github.io/create-react-app/">Create React App</a> generator. We’ll specifically focus on-</p>
<ol>
<li>Setting up a development environment with code hot-reloading</li>
<li>Configuring a production-ready image using multistage builds</li>
</ol>
<div style="text-align:center;">
<img src="/assets/img/blog/docker-logo.png" style="max-width: 100%; border:0; box-shadow: none;" alt="docker" />
</div>
<p><br /></p>
<p><em>Updates:</em></p>
<ul>
<li>April 2020:
<ul>
<li>Updated to the latest versions of Docker, Node, React, and Nginx.</li>
<li>Removed the Docker Machine section.</li>
<li>Updated the <code class="highlighter-rouge">docker run</code> commands to account for <a href="https://github.com/facebook/create-react-app/issues/8688">changes</a> in <code class="highlighter-rouge">react-scripts</code> v3.4.1.</li>
</ul>
</li>
<li>May 2019:
<ul>
<li>Updated to the latest versions of Docker, Node, React, and Nginx.</li>
<li>Added explanations for various Docker commands and flags.</li>
<li>Added a number of notes based on reader comments and feedback.</li>
</ul>
</li>
<li>Feb 2018:
<ul>
<li>Updated to the latest versions of Node, React, and Nginx.</li>
<li>Added an anonymous volume.</li>
<li>Detailed how to configure Nginx to work properly with React Router.</li>
<li>Added a production build section that uses multistage Docker builds.</li>
</ul>
</li>
</ul>
<p><em>We will be using:</em></p>
<ul>
<li>Docker v19.03.8.</li>
<li>Create React App v3.4.1</li>
<li>Node v13.12.0</li>
</ul>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#docker" id="markdown-toc-docker">Docker</a></li>
<li><a href="#production" id="markdown-toc-production">Production</a></li>
<li><a href="#react-router-and-nginx" id="markdown-toc-react-router-and-nginx">React Router and Nginx</a></li>
<li><a href="#next-steps" id="markdown-toc-next-steps">Next Steps</a></li>
</ul>
<h2 id="project-setup">Project Setup</h2>
<p>Install <a href="https://github.com/facebookincubator/create-react-app">Create React App</a> globally:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install <span class="nt">-g</span> create-react-app@3.4.1
</code></pre></div></div>
<p>Generate a new app:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm init react-app sample <span class="nt">--use-npm</span>
<span class="nv">$ </span><span class="nb">cd </span>sample
</code></pre></div></div>
<h2 id="docker">Docker</h2>
<p>Add a <em>Dockerfile</em> to the project root:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># pull official base image</span>
<span class="k">FROM</span><span class="s"> node:13.12.0-alpine</span>
<span class="c"># set working directory</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="c"># add `/app/node_modules/.bin` to $PATH</span>
<span class="k">ENV</span><span class="s"> PATH /app/node_modules/.bin:$PATH</span>
<span class="c"># install app dependencies</span>
<span class="k">COPY</span><span class="s"> package.json ./</span>
<span class="k">COPY</span><span class="s"> package-lock.json ./</span>
<span class="k">RUN </span>npm install <span class="nt">--silent</span>
<span class="k">RUN </span>npm install react-scripts@3.4.1 <span class="nt">-g</span> <span class="nt">--silent</span>
<span class="c"># add app</span>
<span class="k">COPY</span><span class="s"> . ./</span>
<span class="c"># start app</span>
<span class="k">CMD</span><span class="s"> ["npm", "start"]</span>
</code></pre></div></div>
<blockquote>
<p>Silencing the NPM output, via <code class="highlighter-rouge">--silent</code>, is a personal choice. It’s often frowned upon, though, since it can swallow errors. Keep this in mind so you don’t waste time debugging.</p>
</blockquote>
<p>Add a <em>.dockerignore</em>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>node_modules
build
.dockerignore
Dockerfile
Dockerfile.prod
</code></pre></div></div>
<p>This will speed up the Docker build process as our local dependencies inside the “node_modules” directory will not be sent to the Docker daemon.</p>
<p>Build and tag the Docker image:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker build <span class="nt">-t</span> sample:dev <span class="nb">.</span>
</code></pre></div></div>
<p>Then, spin up the container once the build is done:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="se">\</span>
<span class="nt">-it</span> <span class="se">\</span>
<span class="nt">--rm</span> <span class="se">\</span>
<span class="nt">-v</span> <span class="k">${</span><span class="nv">PWD</span><span class="k">}</span>:/app <span class="se">\</span>
<span class="nt">-v</span> /app/node_modules <span class="se">\</span>
<span class="nt">-p</span> 3001:3000 <span class="se">\</span>
<span class="nt">-e</span> <span class="nv">CHOKIDAR_USEPOLLING</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
sample:dev
</code></pre></div></div>
<blockquote>
<p>If you run into an <code class="highlighter-rouge">"ENOENT: no such file or directory, open '/app/package.json".</code> error, you may need to add an additional volume: <code class="highlighter-rouge">-v /app/package.json</code>.</p>
</blockquote>
<p>What’s happening here?</p>
<ol>
<li>The <a href="https://docs.docker.com/engine/reference/commandline/run/">docker run</a> command creates and runs a new container instance from the image we just created.</li>
<li>
<p><code class="highlighter-rouge">-it</code> starts the container in <a href="https://stackoverflow.com/questions/48368411/what-is-docker-run-it-flag">interactive mode</a>. Why is this necessary? As of <a href="https://github.com/facebook/create-react-app/issues/8688">version 3.4.1</a>, <code class="highlighter-rouge">react-scripts</code> exits after start-up (unless CI mode is specified) which will cause the container to exit. Thus the need for interactive mode.</p>
</li>
<li><code class="highlighter-rouge">--rm</code> <a href="https://docs.docker.com/engine/reference/run/#clean-up---rm">removes</a> the container and volumes after the container exits.</li>
<li>
<p><code class="highlighter-rouge">-v ${PWD}:/app</code> mounts the code into the container at “/app”.</p>
<blockquote>
<p><code class="highlighter-rouge">{PWD}</code> may not work on Windows. See <a href="https://stackoverflow.com/questions/41485217/mount-current-directory-as-a-volume-in-docker-on-windows-10">this</a> Stack Overflow question for more info.</p>
</blockquote>
</li>
<li>Since we want to use the container version of the “node_modules” folder, we configured another volume: <code class="highlighter-rouge">-v /app/node_modules</code>. You should now be able to remove the local “node_modules” flavor.</li>
<li>
<p><code class="highlighter-rouge">-p 3001:3000</code> exposes port 3000 to other Docker containers on the same network (for inter-container communication) and port 3001 to the host.</p>
<blockquote>
<p>For more, review <a href="https://stackoverflow.com/questions/22111060/what-is-the-difference-between-expose-and-publish-in-docker">this</a> Stack Overflow question.</p>
</blockquote>
</li>
<li>Finally, <code class="highlighter-rouge">-e CHOKIDAR_USEPOLLING=true</code> <a href="https://create-react-app.dev/docs/troubleshooting/#npm-start-doesn-t-detect-changes">enables</a> a polling mechanism via <a href="https://github.com/paulmillr/chokidar">chokidar</a> (which wraps <code class="highlighter-rouge">fs.watch</code>, <code class="highlighter-rouge">fs.watchFile</code>, and <code class="highlighter-rouge">fsevents</code>) so that hot-reloading will work.</li>
</ol>
<p>Open your browser to <a href="http://localhost:3001/">http://localhost:3001/</a> and you should see the app. Try making a change to the <code class="highlighter-rouge">App</code> component within your code editor. You should see the app hot-reload. Kill the server once done.</p>
<blockquote>
<p>What happens when you add <code class="highlighter-rouge">d</code> to ` -it`?</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="se">\</span>
<span class="nt">-itd</span> <span class="se">\</span>
<span class="nt">--rm</span> <span class="se">\</span>
<span class="nt">-v</span> <span class="k">${</span><span class="nv">PWD</span><span class="k">}</span>:/app <span class="se">\</span>
<span class="nt">-v</span> /app/node_modules <span class="se">\</span>
<span class="nt">-p</span> 3001:3000 <span class="se">\</span>
<span class="nt">-e</span> <span class="nv">CHOKIDAR_USEPOLLING</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
sample:dev
</code></pre></div> </div>
<p>Check your understanding and look this up on your own.</p>
</blockquote>
<p>Want to use <a href="https://docs.docker.com/compose/">Docker Compose</a>? Add a <em>docker-compose.yml</em> file to the project root:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.7'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">sample</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">sample</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">.:/app'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">/app/node_modules'</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">3001:3000</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">CHOKIDAR_USEPOLLING=true</span>
</code></pre></div></div>
<p>Take note of the volumes. Without the <a href="https://success.docker.com/article/Different_Types_of_Volumes">anonymous</a> volume (<code class="highlighter-rouge">'/app/node_modules'</code>), the <em>node_modules</em> directory would be overwritten by the mounting of the host directory at runtime. In other words, this would happen:</p>
<ul>
<li><em>Build</em> - The <code class="highlighter-rouge">node_modules</code> directory is created in the image.</li>
<li><em>Run</em> - The current directory is mounted into the container, overwriting the <code class="highlighter-rouge">node_modules</code> that were installed during the build.</li>
</ul>
<p>Build the image and fire up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">-d</span> <span class="nt">--build</span>
</code></pre></div></div>
<p>Ensure the app is running in the browser and test hot-reloading again. Bring down the container before moving on:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose stop
</code></pre></div></div>
<blockquote>
<p><em>Windows Users</em>: Having problems getting the volumes to work properly? Review the following resources:</p>
<ol>
<li><a href="https://rominirani.com/docker-on-windows-mounting-host-directories-d96f3f056a2c">Docker on Windows–Mounting Host Directories</a></li>
<li><a href="https://blogs.msdn.microsoft.com/stevelasker/2016/06/14/configuring-docker-for-windows-volumes/">Configuring Docker for Windows Shared Drives</a></li>
</ol>
<p>You also may need to add <code class="highlighter-rouge">COMPOSE_CONVERT_WINDOWS_PATHS=1</code> to the environment portion of your Docker Compose file. Review the <a href="https://docs.docker.com/compose/env-file/">Declare default environment variables in file</a> guide for more info.</p>
</blockquote>
<h2 id="production">Production</h2>
<p>Let’s create a separate Dockerfile for use in production called <em>Dockerfile.prod</em>:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># build environment</span>
<span class="k">FROM</span><span class="s"> node:13.12.0-alpine as build</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">ENV</span><span class="s"> PATH /app/node_modules/.bin:$PATH</span>
<span class="k">COPY</span><span class="s"> package.json ./</span>
<span class="k">COPY</span><span class="s"> package-lock.json ./</span>
<span class="k">RUN </span>npm ci <span class="nt">--silent</span>
<span class="k">RUN </span>npm install react-scripts@3.4.1 <span class="nt">-g</span> <span class="nt">--silent</span>
<span class="k">COPY</span><span class="s"> . ./</span>
<span class="k">RUN </span>npm run build
<span class="c"># production environment</span>
<span class="k">FROM</span><span class="s"> nginx:stable-alpine</span>
<span class="k">COPY</span><span class="s"> --from=build /app/build /usr/share/nginx/html</span>
<span class="k">EXPOSE</span><span class="s"> 80</span>
<span class="k">CMD</span><span class="s"> ["nginx", "-g", "daemon off;"]</span>
</code></pre></div></div>
<p>Here, we take advantage of the <a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">multistage build</a> pattern to create a temporary image used for building the artifact – the production-ready React static files – that is then copied over to the production image. The temporary build image is discarded along with the original files and folders associated with the image. This produces a lean, production-ready image.</p>
<blockquote>
<p>Check out the <a href="https://blog.alexellis.io/mutli-stage-docker-builds/">Builder pattern vs. Multi-stage builds in Docker</a> blog post for more info on multistage builds.</p>
</blockquote>
<p>Using the production Dockerfile, build and tag the Docker image:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker build <span class="nt">-f</span> Dockerfile.prod <span class="nt">-t</span> sample:prod <span class="nb">.</span>
</code></pre></div></div>
<p>Spin up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="nt">-p</span> 1337:80 sample:prod
</code></pre></div></div>
<p>Navigate to <a href="http://localhost:1337/">http://localhost:1337/</a> in your browser to view the app.</p>
<p>Test with a new Docker Compose file as well called <em>docker-compose.prod.yml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.7'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">sample-prod</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">sample-prod</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile.prod</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">1337:80'</span>
</code></pre></div></div>
<p>Fire up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose <span class="nt">-f</span> docker-compose.prod.yml up <span class="nt">-d</span> <span class="nt">--build</span>
</code></pre></div></div>
<p>Test it out once more in your browser.</p>
<h2 id="react-router-and-nginx">React Router and Nginx</h2>
<p>If you’re using <a href="https://reacttraining.com/react-router/">React Router</a>, then you’ll need to change the default Nginx config at build time:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">COPY</span><span class="s"> --from=build /app/build /usr/share/nginx/html</span>
</code></pre></div></div>
<p>Add the change to <em>Dockerfile.prod</em>:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># build environment</span>
<span class="k">FROM</span><span class="s"> node:13.12.0-alpine as build</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">ENV</span><span class="s"> PATH /app/node_modules/.bin:$PATH</span>
<span class="k">COPY</span><span class="s"> package.json ./</span>
<span class="k">COPY</span><span class="s"> package-lock.json ./</span>
<span class="k">RUN </span>npm ci <span class="nt">--silent</span>
<span class="k">RUN </span>npm install react-scripts@3.4.1 <span class="nt">-g</span> <span class="nt">--silent</span>
<span class="k">COPY</span><span class="s"> . ./</span>
<span class="k">RUN </span>npm run build
<span class="c"># production environment</span>
<span class="k">FROM</span><span class="s"> nginx:stable-alpine</span>
<span class="k">COPY</span><span class="s"> --from=build /app/build /usr/share/nginx/html</span>
<span class="c"># new</span>
<span class="k">COPY</span><span class="s"> nginx/nginx.conf /etc/nginx/conf.d/default.conf</span>
<span class="k">EXPOSE</span><span class="s"> 80</span>
<span class="k">CMD</span><span class="s"> ["nginx", "-g", "daemon off;"]</span>
</code></pre></div></div>
<p>Create the following folder along with a <em>nginx.conf</em> file:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>└── nginx
└── nginx.conf
</code></pre></div></div>
<p><em>nginx.conf</em>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
</code></pre></div></div>
<h2 id="next-steps">Next Steps</h2>
<p>With that, you should now be able to add React to a larger Docker-powered project for both development and production environments. If you’d like to learn more about working with React and Docker along with building and testing microservices, check out the <a href="https://testdriven.io/bundle/microservices-with-docker-flask-and-react/">Microservices with Docker, Flask, and React</a> course bundle at <a href="https://testdriven.io">TestDriven.io</a>.</p>
Michael Herman
Docker is a containerization tool used to speed up the development and deployment processes. If you’re working with microservices, Docker makes it much easier to link together small, independent services. It also helps to eliminate environment-specific bugs since you can replicate your production environment locally.
Deploying a Jekyll Site to Netlify with Docker and GitLab CI
2020-03-01T00:00:00-06:00
2020-03-01T00:00:00-06:00
https://mherman.org/blog/deploying-jekyll-netlify-docker-gitlab-ci
<p>This is a step-by-step guide covering how to automatically deploy a <a href="https://jekyllrb.com/">Jekyll</a> site to <a href="https://www.netlify.com/">Netlify</a> using <a href="https://www.docker.com/">Docker</a> and <a href="https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/">GitLab CI/CD</a>.</p>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#assumptions" id="markdown-toc-assumptions">Assumptions</a></li>
<li><a href="#docker-setup" id="markdown-toc-docker-setup">Docker Setup</a></li>
<li><a href="#gitlab-build" id="markdown-toc-gitlab-build">GitLab Build</a></li>
<li><a href="#netlify-api-deployment" id="markdown-toc-netlify-api-deployment">Netlify API Deployment</a></li>
<li><a href="#gitlab-deploy" id="markdown-toc-gitlab-deploy">GitLab Deploy</a></li>
</ul>
<h2 id="assumptions">Assumptions</h2>
<p>This post assumes that have already set up a GitLab repository and a Netlify site. Your Jekyll site should have the following project structure as well:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── .gitignore
└── src
├── 404.html
├── Gemfile
├── Gemfile.lock
├── _config.yml
├── _posts
├── about.markdown
└── index.markdown
</code></pre></div></div>
<h2 id="docker-setup">Docker Setup</h2>
<p>Let’s start by setting up a Dockerfile based on the <a href="https://hub.docker.com/r/jekyll/jekyll/">jekyll/jekyll</a> Docker image to manage a compatible Ruby version for Jekyll along with <a href="https://bundler.io/">bundler</a> and all the <a href="https://rubygems.org/">RubyGems</a>.</p>
<p>Add the <em>Dockerfile</em> to the project root:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> jekyll/jekyll:3.8.0</span>
<span class="k">WORKDIR</span><span class="s"> /tmp</span>
<span class="k">ENV</span><span class="s"> BUNDLER_VERSION 2.1.4</span>
<span class="k">ENV</span><span class="s"> NOKOGIRI_USE_SYSTEM_LIBRARIES 1</span>
<span class="k">ADD</span><span class="s"> ./src/Gemfile /tmp/</span>
<span class="k">ADD</span><span class="s"> ./src/Gemfile.lock /tmp/</span>
<span class="k">RUN </span>gem install bundler <span class="nt">-i</span> /usr/gem <span class="nt">-v</span> 2.1.4
<span class="k">RUN </span>bundle install
<span class="k">WORKDIR</span><span class="s"> /srv/jekyll</span>
</code></pre></div></div>
<p>Build and tag the image:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker build <span class="nt">--tag</span> jekyll-docker <span class="nb">.</span>
</code></pre></div></div>
<p>Once built, spin up the container like so to serve up the site locally on port 4000:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="se">\</span>
<span class="nt">-d</span> <span class="nt">-v</span> <span class="nv">$PWD</span>/src:/srv/jekyll <span class="nt">-p</span> 4000:4000 <span class="se">\</span>
jekyll-docker bundle <span class="nb">exec </span>jekyll serve <span class="nt">-H</span> 0.0.0.0
</code></pre></div></div>
<p>Make sure the site is up at <a href="http://localhost:4000/">http://localhost:4000/</a>.</p>
<p>Bring down the container once done:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sh docker stop $(docker ps -q --filter ancestor=jekyll-docker)
</code></pre></div></div>
<h2 id="gitlab-build">GitLab Build</h2>
<p>With that, to configure the GitLab CI pipeline associated with the repo, add a <em>.gitlab-ci.yml</em> file to the project root:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">image</span><span class="pi">:</span> <span class="s">docker:stable</span>
<span class="na">services</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">docker:dind</span>
<span class="na">stages</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">build</span>
<span class="na">variables</span><span class="pi">:</span>
<span class="na">IMAGE</span><span class="pi">:</span> <span class="s">${CI_REGISTRY}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">stage</span><span class="pi">:</span> <span class="s">build</span>
<span class="na">script</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">docker login -u $CI_REGISTRY_USER -p $CI_JOB_TOKEN $CI_REGISTRY</span>
<span class="pi">-</span> <span class="s">docker pull $IMAGE:latest || </span><span class="no">true</span>
<span class="pi">-</span> <span class="s">docker build --cache-from $IMAGE:latest --tag $IMAGE:latest .</span>
<span class="pi">-</span> <span class="s">docker push $IMAGE:latest</span>
<span class="pi">-</span> <span class="s">docker run -v $PWD/src:/srv/jekyll $IMAGE:latest bundle exec jekyll build</span>
</code></pre></div></div>
<p>Here, using <a href="https://hub.docker.com/_/docker">Docker-in-Docker</a>, we defined a single <a href="https://docs.gitlab.com/ee/ci/yaml/#stages">stage</a> called <code class="highlighter-rouge">build</code> that:</p>
<ol>
<li>Logs in to the GitLab Container Registry</li>
<li>Pulls the previously pushed image (if it exists)</li>
<li>Builds and tags the new image</li>
<li>Pushes the image up to the GitLab <a href="https://docs.gitlab.com/ee/user/packages/container_registry/">Container Registry</a></li>
<li>Creates a Jekyll build</li>
</ol>
<p>Commit your code and push it up to GitLab. This should trigger a new build, which should pass. You should also see the image in the Container Registry:</p>
<div style="text-align:center;">
<img src="/assets/img/blog/jekyll-netlify-gitlab/gitlab-container-registry.png" style="max-width:90%;border:0;box-shadow:none;margin-bottom:20px;" alt="gitlab container registry" />
</div>
<p>This first build should take between five to six minutes to complete. Subsequent builds will be much faster since they will leverage Docker layer caching.</p>
<blockquote>
<p>For more on caching check out <a href="https://testdriven.io/blog/faster-ci-builds-with-docker-cache/">Faster CI Builds with Docker Cache</a>.</p>
</blockquote>
<h2 id="netlify-api-deployment">Netlify API Deployment</h2>
<p>Next, to use the <a href="https://docs.netlify.com/api/get-started/">Netlify API</a> to <a href="https://docs.netlify.com/api/get-started/#deploy-via-api">deploy</a> the Jekyll site, add the following to a <em>deploy.sh</em> script in the project root:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>
zip <span class="nt">-r</span> website.zip ./src/_site
curl <span class="nt">-H</span> <span class="s2">"Content-Type: application/zip"</span> <span class="se">\</span>
<span class="nt">-H</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$NETLIFY_ACCESS_TOKEN</span><span class="s2">"</span> <span class="se">\</span>
<span class="nt">--data-binary</span> <span class="s2">"@website.zip"</span> <span class="se">\</span>
https://api.netlify.com/api/v1/sites/<span class="nv">$NETLIFY_SUBDOMAIN</span>.netlify.com/deploys
</code></pre></div></div>
<p>To test locally, you’ll first need to create an access token (if you haven’t already done so), which can be <a href="https://docs.netlify.com/cli/get-started/#authentication">obtained</a> from either the command line or the Netlify UI.</p>
<p>Once obtained, set it as an environment variable along with your Netlify subdomain:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">export </span><span class="nv">NETLIFY_ACCESS_TOKEN</span><span class="o">=</span><your_access_token>
<span class="nv">$ </span><span class="nb">export </span><span class="nv">NETLIFY_SUBDOMAIN</span><span class="o">=</span><your_subdomain>
</code></pre></div></div>
<p>Generate the static files:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="se">\</span>
<span class="nt">-v</span> <span class="nv">$PWD</span>/src:/srv/jekyll <span class="nt">-p</span> 4000:4000 <span class="se">\</span>
jekyll-docker bundle <span class="nb">exec </span>jekyll build
</code></pre></div></div>
<p>Make sure the “src/_site” directory was created before deploying the site:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>chmod +x deploy.sh
<span class="nv">$ </span>./deploy.sh
</code></pre></div></div>
<p>So, after zipping the “src/_site” directory, we sent a POST request to <code class="highlighter-rouge">https://api.netlify.com/api/v1/sites/$NETLIFY_SUBDOMAIN.netlify.com/deploys</code> with the zip file in the HTTP request body.</p>
<p>Make sure the site was deployed before moving on.</p>
<h2 id="gitlab-deploy">GitLab Deploy</h2>
<p>Finally, to automate the deploy, add a new stage to the <em>.gitlab-ci.yml</em> file, called <code class="highlighter-rouge">deploy</code>, to deploy the site to Netlify after a successful build:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">image</span><span class="pi">:</span> <span class="s">docker:stable</span>
<span class="na">services</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">docker:dind</span>
<span class="na">stages</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">build</span>
<span class="pi">-</span> <span class="s">deploy</span>
<span class="na">variables</span><span class="pi">:</span>
<span class="na">IMAGE</span><span class="pi">:</span> <span class="s">${CI_REGISTRY}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">stage</span><span class="pi">:</span> <span class="s">build</span>
<span class="na">script</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">docker login -u $CI_REGISTRY_USER -p $CI_JOB_TOKEN $CI_REGISTRY</span>
<span class="pi">-</span> <span class="s">docker pull $IMAGE:latest || </span><span class="no">true</span>
<span class="pi">-</span> <span class="s">docker build --cache-from $IMAGE:latest --tag $IMAGE:latest .</span>
<span class="pi">-</span> <span class="s">docker push $IMAGE:latest</span>
<span class="pi">-</span> <span class="s">docker run -v $PWD/src:/srv/jekyll $IMAGE:latest bundle exec jekyll build</span>
<span class="na">artifacts</span><span class="pi">:</span>
<span class="na">paths</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">src/_site</span>
<span class="na">deploy</span><span class="pi">:</span>
<span class="na">stage</span><span class="pi">:</span> <span class="s">deploy</span>
<span class="na">script</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">apk add --update zip curl</span>
<span class="pi">-</span> <span class="s">chmod +x ./deploy.sh</span>
<span class="pi">-</span> <span class="s">/bin/sh ./deploy.sh</span>
<span class="na">artifacts</span><span class="pi">:</span>
<span class="na">paths</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">src/_site</span>
</code></pre></div></div>
<p>Take note of the new <a href="https://docs.gitlab.com/ee/user/project/pipelines/job_artifacts.html">artifacts</a> definition added to the <code class="highlighter-rouge">build</code> stage:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">artifacts</span><span class="pi">:</span>
<span class="na">paths</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">src/_site</span>
</code></pre></div></div>
<p>If the <code class="highlighter-rouge">build</code> stage succeeds, the generated static files from the <em>src/_site</em> directory – the result of <code class="highlighter-rouge">docker run -v $PWD/src:/srv/jekyll $IMAGE:latest bundle exec jekyll build</code> – will be passed on to subsequent stages.</p>
<p>Add the <code class="highlighter-rouge">NETLIFY_ACCESS_TOKEN</code> and <code class="highlighter-rouge">NETLIFY_SUBDOMAIN</code> variables to your project’s CI/CD settings: Settings > CI / CD > Variables:</p>
<div style="text-align:center;">
<img src="/assets/img/blog/jekyll-netlify-gitlab/gitlab-variables.png" style="max-width:90%;border:0;box-shadow:none;margin-bottom:20px;" alt="gitlab variables" />
</div>
<p>Commit your code and push it up again to GitLab to trigger a new build. After the <code class="highlighter-rouge">build</code> stage completes, you should be able to see the artifact on the job page:</p>
<div style="text-align:center;">
<img src="/assets/img/blog/jekyll-netlify-gitlab/gitlab-job-artifact.png" style="max-width:90%;border:0;box-shadow:none;margin-bottom:20px;" alt="gitlab job artifact" />
</div>
<p>The site should be deployed during the <code class="highlighter-rouge">deploy</code> stage.</p>
<hr />
<p>You can find the code in the <a href="https://gitlab.com/michaelherman/jekyll-netlify-gitlab">jekyll-netlify-gitlab</a> repo on GitLab.</p>
Michael Herman
This is a step-by-step guide covering how to automatically deploy a Jekyll site to Netlify using Docker and GitLab CI/CD.
Dockerizing a Vue App
2019-05-21T00:00:00-05:00
2019-05-21T00:00:00-05:00
https://mherman.org/blog/dockerizing-a-vue-app
<p>This tutorial looks at how to Dockerize a <a href="https://vuejs.org/">Vue</a> app, built with the <a href="https://cli.vuejs.org/">Vue CLI</a>, using Docker along with Docker Compose and Docker Machine for both development and production. We’ll specifically focus on-</p>
<ol>
<li>Setting up a development environment with code hot-reloading</li>
<li>Configuring a production-ready image using multistage builds</li>
</ol>
<p><em>We will be using:</em></p>
<ul>
<li>Docker v18.09.2</li>
<li>Vue CLI v3.7.0</li>
<li>Node v12.2.0</li>
</ul>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#docker" id="markdown-toc-docker">Docker</a></li>
<li><a href="#docker-machine" id="markdown-toc-docker-machine">Docker Machine</a></li>
<li><a href="#production" id="markdown-toc-production">Production</a></li>
<li><a href="#vue-router-and-nginx" id="markdown-toc-vue-router-and-nginx">Vue Router and Nginx</a></li>
</ul>
<h2 id="project-setup">Project Setup</h2>
<p>Install the <a href="https://cli.vuejs.org/">Vue CLI</a> globally:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install <span class="nt">-g</span> @vue/cli@3.7.0
</code></pre></div></div>
<p>Generate a new app, using the <a href="https://cli.vuejs.org/guide/creating-a-project.html">default preset</a>:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>vue create my-app <span class="nt">--default</span>
<span class="nv">$ </span><span class="nb">cd </span>my-app
</code></pre></div></div>
<h2 id="docker">Docker</h2>
<p>Add a <em>Dockerfile</em> to the project root:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># base image</span>
<span class="k">FROM</span><span class="s"> node:12.2.0-alpine</span>
<span class="c"># set working directory</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="c"># add `/app/node_modules/.bin` to $PATH</span>
<span class="k">ENV</span><span class="s"> PATH /app/node_modules/.bin:$PATH</span>
<span class="c"># install and cache app dependencies</span>
<span class="k">COPY</span><span class="s"> package.json /app/package.json</span>
<span class="k">RUN </span>npm install
<span class="k">RUN </span>npm install @vue/cli@3.7.0 <span class="nt">-g</span>
<span class="c"># start app</span>
<span class="k">CMD</span><span class="s"> ["npm", "run", "serve"]</span>
</code></pre></div></div>
<p>Add a <em>.dockerignore</em> as well:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>node_modules
.git
.gitignore
</code></pre></div></div>
<p>This will speed up the Docker build process as our local dependencies and git repo will not be sent to the Docker daemon.</p>
<p>Build and tag the Docker image:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker build <span class="nt">-t</span> my-app:dev <span class="nb">.</span>
</code></pre></div></div>
<p>Then, spin up the container once the build is done:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-v</span> <span class="k">${</span><span class="nv">PWD</span><span class="k">}</span>:/app <span class="nt">-v</span> /app/node_modules <span class="nt">-p</span> 8081:8080 <span class="nt">--rm</span> my-app:dev
</code></pre></div></div>
<p>What’s happening here?</p>
<ol>
<li>The <a href="https://docs.docker.com/engine/reference/commandline/run/">docker run</a> command creates a new container instance, from the image we just created, and runs it.</li>
<li>
<p><code class="highlighter-rouge">-v ${PWD}:/app</code> mounts the code into the container at “/app”.</p>
<blockquote>
<p><code class="highlighter-rouge">{PWD}</code> may not work on Windows. See <a href="https://stackoverflow.com/questions/41485217/mount-current-directory-as-a-volume-in-docker-on-windows-10">this</a> Stack Overflow question for more info.</p>
</blockquote>
</li>
<li>Since we want to use the container version of the “node_modules” folder, we configured another volume: <code class="highlighter-rouge">-v /app/node_modules</code>. You should now be able to remove the local “node_modules” flavor.</li>
<li>
<p><code class="highlighter-rouge">-p 8081:8080</code> exposes port 8080 to other Docker containers on the same network (for inter-container communication) and port 8081 to the host.</p>
<blockquote>
<p>For more, review <a href="https://stackoverflow.com/questions/22111060/what-is-the-difference-between-expose-and-publish-in-docker">this</a> Stack Overflow question.</p>
</blockquote>
</li>
<li>Finally, <code class="highlighter-rouge">--rm</code> <a href="https://docs.docker.com/engine/reference/run/#clean-up---rm">removes</a> the container and volumes after the container exits.</li>
</ol>
<p>Open your browser to <a href="http://localhost:8081">http://localhost:8081</a> and you should see the app. Try making a change to the <code class="highlighter-rouge">App</code> component (<em>src/App.vue</em>) within your code editor. You should see the app hot-reload. Kill the server once done.</p>
<blockquote>
<p>What happens when you add <code class="highlighter-rouge">-it</code>?</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-it</span> <span class="nt">-v</span> <span class="k">${</span><span class="nv">PWD</span><span class="k">}</span>:/app <span class="nt">-v</span> /app/node_modules <span class="nt">-p</span> 8081:8080 <span class="nt">--rm</span> my-app:dev
</code></pre></div> </div>
<p>Check your understanding and look this up on your own.</p>
</blockquote>
<p>Want to use <a href="https://docs.docker.com/compose/">Docker Compose</a>? Add a <em>docker-compose.yml</em> file to the project root:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.7'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">my-app</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">my-app</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">.:/app'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">/app/node_modules'</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">8081:8080'</span>
</code></pre></div></div>
<p>Take note of the volumes. Without the <a href="https://success.docker.com/article/Different_Types_of_Volumes">anonymous</a> volume (<code class="highlighter-rouge">'/app/node_modules'</code>), the <em>node_modules</em> directory would be overwritten by the mounting of the host directory at runtime. In other words, this would happen:</p>
<ul>
<li><em>Build</em> - The <code class="highlighter-rouge">node_modules</code> directory is created in the image.</li>
<li><em>Run</em> - The current directory is mounted into the container, overwriting the <code class="highlighter-rouge">node_modules</code> that were installed during the build.</li>
</ul>
<p>Build the image and fire up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">-d</span> <span class="nt">--build</span>
</code></pre></div></div>
<p>Ensure the app is running in the browser and test hot-reloading again. Bring down the container before moving on:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose stop
</code></pre></div></div>
<blockquote>
<p><em>Windows Users</em>: Having problems getting the volumes to work properly? Review the following resources:</p>
<ol>
<li><a href="https://rominirani.com/docker-on-windows-mounting-host-directories-d96f3f056a2c">Docker on Windows–Mounting Host Directories</a></li>
<li><a href="https://blogs.msdn.microsoft.com/stevelasker/2016/06/14/configuring-docker-for-windows-volumes/">Configuring Docker for Windows Shared Drives</a></li>
</ol>
<p>You also may need to add <code class="highlighter-rouge">COMPOSE_CONVERT_WINDOWS_PATHS=1</code> to the environment portion of your Docker Compose file. Review the <a href="https://docs.docker.com/compose/env-file/">Declare default environment variables in file</a> guide for more info.</p>
</blockquote>
<h2 id="docker-machine">Docker Machine</h2>
<p>To get hot-reloading to work with <a href="https://docs.docker.com/machine/">Docker Machine</a> and <a href="https://docs.docker.com/machine/get-started/">VirtualBox</a> you’ll need to enable a polling mechanism via <a href="https://github.com/paulmillr/chokidar">chokidar</a> (which wraps <code class="highlighter-rouge">fs.watch</code>, <code class="highlighter-rouge">fs.watchFile</code>, and <code class="highlighter-rouge">fsevents</code>).</p>
<p>Create a new Machine:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-machine create <span class="nt">-d</span> virtualbox my-app
<span class="nv">$ </span>docker-machine env my-app
<span class="nv">$ </span><span class="nb">eval</span> <span class="k">$(</span>docker-machine env my-app<span class="k">)</span>
</code></pre></div></div>
<p>Grab the IP address:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-machine ip my-app
</code></pre></div></div>
<p>Then, build the images:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker build <span class="nt">-t</span> my-app:dev <span class="nb">.</span>
</code></pre></div></div>
<p>And run the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-it</span> <span class="nt">-v</span> <span class="k">${</span><span class="nv">PWD</span><span class="k">}</span>:/app <span class="nt">-v</span> /app/node_modules <span class="nt">-p</span> 8081:8080 <span class="nt">--rm</span> my-app:dev
</code></pre></div></div>
<p>Test the app again in the browser at <a href="http://DOCKER_MACHINE_IP:8081">http://DOCKER_MACHINE_IP:8081</a> (make sure to replace <code class="highlighter-rouge">DOCKER_MACHINE_IP</code> with the actual IP address of the Docker Machine). Also, confirm that auto reload is <em>not</em> working. You can try with Docker Compose as well, but the result will be the same.</p>
<p>To get hot-reload working, we need to add an environment variable: <code class="highlighter-rouge">CHOKIDAR_USEPOLLING=true</code>.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-it</span> <span class="nt">-v</span> <span class="k">${</span><span class="nv">PWD</span><span class="k">}</span>:/app <span class="nt">-v</span> /app/node_modules <span class="nt">-p</span> 8081:8080 <span class="nt">-e</span> <span class="nv">CHOKIDAR_USEPOLLING</span><span class="o">=</span><span class="nb">true</span> <span class="nt">--rm</span> my-app:dev
</code></pre></div></div>
<p>Test it out again. Then, kill the server and add the environment variable to the <em>docker-compose.yml</em> file:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.7'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">my-app</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">my-app</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">.:/app'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">/app/node_modules'</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">8081:8080'</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">CHOKIDAR_USEPOLLING=true</span>
</code></pre></div></div>
<h2 id="production">Production</h2>
<p>Let’s create a separate Dockerfile for use in production called <em>Dockerfile-prod</em>:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># build environment</span>
<span class="k">FROM</span><span class="s"> node:12.2.0-alpine as build</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">ENV</span><span class="s"> PATH /app/node_modules/.bin:$PATH</span>
<span class="k">COPY</span><span class="s"> package.json /app/package.json</span>
<span class="k">RUN </span>npm install <span class="nt">--silent</span>
<span class="k">RUN </span>npm install @vue/cli@3.7.0 <span class="nt">-g</span>
<span class="k">COPY</span><span class="s"> . /app</span>
<span class="k">RUN </span>npm run build
<span class="c"># production environment</span>
<span class="k">FROM</span><span class="s"> nginx:1.16.0-alpine</span>
<span class="k">COPY</span><span class="s"> --from=build /app/dist /usr/share/nginx/html</span>
<span class="k">EXPOSE</span><span class="s"> 80</span>
<span class="k">CMD</span><span class="s"> ["nginx", "-g", "daemon off;"]</span>
</code></pre></div></div>
<p>Here, we take advantage of the <a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">multistage build</a> pattern to create a temporary image used for building the artifact – the production-ready Vue static files – that is then copied over to the production image. The temporary build image is discarded along with the original files and folders associated with the image. This produces a lean, production-ready image.</p>
<blockquote>
<p>Check out the <a href="https://blog.alexellis.io/mutli-stage-docker-builds/">Builder pattern vs. Multi-stage builds in Docker</a> blog post for more info on multistage builds.</p>
</blockquote>
<p>Using the production Dockerfile, build and tag the Docker image:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker build <span class="nt">-f</span> Dockerfile-prod <span class="nt">-t</span> my-app:prod <span class="nb">.</span>
</code></pre></div></div>
<p>Spin up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-it</span> <span class="nt">-p</span> 80:80 <span class="nt">--rm</span> my-app:prod
</code></pre></div></div>
<p>Assuming you are still using the same Docker Machine, navigate to <a href="http://DOCKER_MACHINE_IP/">http://DOCKER_MACHINE_IP/</a> in your browser.</p>
<p>Test with a new Docker Compose file as well called <em>docker-compose-prod.yml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.7'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">my-app-prod</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">my-app-prod</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile-prod</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">80:80'</span>
</code></pre></div></div>
<p>Fire up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose <span class="nt">-f</span> docker-compose-prod.yml up <span class="nt">-d</span> <span class="nt">--build</span>
</code></pre></div></div>
<p>Test it out once more in your browser.</p>
<p>If you’re done, go ahead and destroy the Machine:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">eval</span> <span class="k">$(</span>docker-machine env <span class="nt">-u</span><span class="k">)</span>
<span class="nv">$ </span>docker-machine rm my-app
</code></pre></div></div>
<h2 id="vue-router-and-nginx">Vue Router and Nginx</h2>
<p>If you’re using <a href="https://router.vuejs.org/">Vue Router</a>, then you’ll need to change the default Nginx config at build time:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">RUN </span>rm /etc/nginx/conf.d/default.conf
<span class="k">COPY</span><span class="s"> nginx/nginx.conf /etc/nginx/conf.d</span>
</code></pre></div></div>
<p>Add the changes to <em>Dockerfile-prod</em>:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># build environment</span>
<span class="k">FROM</span><span class="s"> node:12.2.0-alpine as build</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">ENV</span><span class="s"> PATH /app/node_modules/.bin:$PATH</span>
<span class="k">COPY</span><span class="s"> package.json /app/package.json</span>
<span class="k">RUN </span>npm install <span class="nt">--silent</span>
<span class="k">RUN </span>npm install @vue/cli@3.7.0 <span class="nt">-g</span>
<span class="k">COPY</span><span class="s"> . /app</span>
<span class="k">RUN </span>npm run build
<span class="c"># production environment</span>
<span class="k">FROM</span><span class="s"> nginx:1.16.0-alpine</span>
<span class="k">COPY</span><span class="s"> --from=build /app/dist /usr/share/nginx/html</span>
<span class="k">RUN </span>rm /etc/nginx/conf.d/default.conf
<span class="k">COPY</span><span class="s"> nginx/nginx.conf /etc/nginx/conf.d</span>
<span class="k">EXPOSE</span><span class="s"> 80</span>
<span class="k">CMD</span><span class="s"> ["nginx", "-g", "daemon off;"]</span>
</code></pre></div></div>
<p>Create the following folder along with a <em>nginx.conf</em> file:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>└── nginx
└── nginx.conf
</code></pre></div></div>
<p><em>nginx.conf</em>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
</code></pre></div></div>
<hr />
<p>Cheers!</p>
Michael Herman
This tutorial looks at how to Dockerize a Vue app, built with the Vue CLI, using Docker along with Docker Compose and Docker Machine for both development and production. We’ll specifically focus on-
Dockerizing an Angular App
2019-05-20T00:00:00-05:00
2019-05-20T00:00:00-05:00
https://mherman.org/blog/dockerizing-an-angular-app
<p><a href="https://www.docker.com/">Docker</a> is a containerization tool used to streamline application development and deployment workflows across various environments.</p>
<p>This tutorial shows how to Dockerize an <a href="https://angular.io/">Angular</a> app, built with the <a href="https://cli.angular.io/">Angular CLI</a>, using Docker along with Docker Compose and Docker Machine for both development and production. We’ll specifically focus on-</p>
<ol>
<li>Setting up an image for development with code hot-reloading that includes an instance of Chrome for Karma and Protractor testing</li>
<li>Configuring a lean, production-ready image using multistage builds</li>
</ol>
<p><em>Updates:</em></p>
<ul>
<li>May 2019:
<ul>
<li>Updated to the latest versions of Docker, Node, Angular, and Nginx.</li>
<li>Added explanations for various Docker commands and flags.</li>
<li>Added a number of notes based on reader comments and feedback.</li>
<li>Fixed the running of Protractor e2e tests.</li>
</ul>
</li>
</ul>
<p><em>We will be using:</em></p>
<ul>
<li>Docker v18.09.2</li>
<li>Angular CLI v7.3.9</li>
<li>Node v12.2.0</li>
</ul>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#docker" id="markdown-toc-docker">Docker</a></li>
<li><a href="#docker-machine" id="markdown-toc-docker-machine">Docker Machine</a></li>
<li><a href="#production" id="markdown-toc-production">Production</a></li>
</ul>
<h2 id="project-setup">Project Setup</h2>
<p>Install the <a href="https://github.com/angular/angular-cli">Angular CLI</a> globally:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install <span class="nt">-g</span> @angular/cli@7.3.9
</code></pre></div></div>
<p>Generate a new app:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ng new example
<span class="nv">$ </span><span class="nb">cd </span>example
</code></pre></div></div>
<h2 id="docker">Docker</h2>
<p>Add a <em>Dockerfile</em> to the project root:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># base image</span>
<span class="k">FROM</span><span class="s"> node:12.2.0</span>
<span class="c"># install chrome for protractor tests</span>
<span class="k">RUN </span>wget <span class="nt">-q</span> <span class="nt">-O</span> - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
<span class="k">RUN </span>sh <span class="nt">-c</span> <span class="s1">'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'</span>
<span class="k">RUN </span>apt-get update <span class="o">&&</span> apt-get install <span class="nt">-yq</span> google-chrome-stable
<span class="c"># set working directory</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="c"># add `/app/node_modules/.bin` to $PATH</span>
<span class="k">ENV</span><span class="s"> PATH /app/node_modules/.bin:$PATH</span>
<span class="c"># install and cache app dependencies</span>
<span class="k">COPY</span><span class="s"> package.json /app/package.json</span>
<span class="k">RUN </span>npm install
<span class="k">RUN </span>npm install <span class="nt">-g</span> @angular/cli@7.3.9
<span class="c"># add app</span>
<span class="k">COPY</span><span class="s"> . /app</span>
<span class="c"># start app</span>
<span class="k">CMD</span><span class="s"> ng serve --host 0.0.0.0</span>
</code></pre></div></div>
<p>Add a <em>.dockerignore</em> as well:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>node_modules
.git
.gitignore
</code></pre></div></div>
<p>This will speed up the Docker build process as our local dependencies and git repo will not be sent to the Docker daemon.</p>
<p>Build and tag the Docker image:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker build <span class="nt">-t</span> example:dev <span class="nb">.</span>
</code></pre></div></div>
<blockquote>
<p>If <code class="highlighter-rouge">RUN npm install -g @angular/cli@7.3.9</code> results in an infinite loop, you may need to add an <code class="highlighter-rouge">--unsafe</code> flag:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RUN npm install -g @angular/cli@7.3.9 --unsafe
</code></pre></div> </div>
<p>Review this <a href="https://github.com/angular/angular-cli/issues/7389">issue</a> for more info.</p>
</blockquote>
<p>Then, spin up the container once the build is done:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-v</span> <span class="k">${</span><span class="nv">PWD</span><span class="k">}</span>:/app <span class="nt">-v</span> /app/node_modules <span class="nt">-p</span> 4201:4200 <span class="nt">--rm</span> example:dev
</code></pre></div></div>
<p>What’s happening here?</p>
<ol>
<li>The <a href="https://docs.docker.com/engine/reference/commandline/run/">docker run</a> command creates a new container instance, from the image we just created, and runs it.</li>
<li>
<p><code class="highlighter-rouge">-v ${PWD}:/app</code> mounts the code into the container at “/app”.</p>
<blockquote>
<p><code class="highlighter-rouge">{PWD}</code> may not work on Windows. See <a href="https://stackoverflow.com/questions/41485217/mount-current-directory-as-a-volume-in-docker-on-windows-10">this</a> Stack Overflow question for more info.</p>
</blockquote>
</li>
<li>Since we want to use the container version of the “node_modules” folder, we configured another volume: <code class="highlighter-rouge">-v /app/node_modules</code>. You should now be able to remove the local “node_modules” flavor.</li>
<li>
<p><code class="highlighter-rouge">-p 4201:4200</code> exposes port 4200 to other Docker containers on the same network (for inter-container communication) and port 4201 to the host.</p>
<blockquote>
<p>For more, review <a href="https://stackoverflow.com/questions/22111060/what-is-the-difference-between-expose-and-publish-in-docker">this</a> Stack Overflow question.</p>
</blockquote>
</li>
<li>Finally, <code class="highlighter-rouge">--rm</code> <a href="https://docs.docker.com/engine/reference/run/#clean-up---rm">removes</a> the container and volumes after the container exits.</li>
</ol>
<p>Open your browser to <a href="http://localhost:4201">http://localhost:4201</a> and you should see the app. Try making a change to the <code class="highlighter-rouge">AppComponent</code>’s template (<em>src/app/app.component.html</em>) within your code editor. You should see the app hot-reload. Kill the server once done.</p>
<blockquote>
<p>What happens when you add <code class="highlighter-rouge">-it</code>?</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-it</span> <span class="nt">-v</span> <span class="k">${</span><span class="nv">PWD</span><span class="k">}</span>:/app <span class="nt">-v</span> /app/node_modules <span class="nt">-p</span> 4201:4200 <span class="nt">--rm</span> example:dev
</code></pre></div> </div>
<p>Check your understanding and look this up on your own.</p>
</blockquote>
<p>Use the <code class="highlighter-rouge">-d</code> flag to run the container in the background:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-d</span> <span class="nt">-v</span> <span class="k">${</span><span class="nv">PWD</span><span class="k">}</span>:/app <span class="nt">-v</span> /app/node_modules <span class="nt">-p</span> 4201:4200 <span class="nt">--name</span> foo <span class="nt">--rm</span> example:dev
</code></pre></div></div>
<p>Once up, update the Karma and Protractor config files to run Chrome in headless mode.</p>
<p><em>src/karma.conf.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">config</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">config</span><span class="p">.</span><span class="kd">set</span><span class="p">({</span>
<span class="na">basePath</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="na">frameworks</span><span class="p">:</span> <span class="p">[</span><span class="s1">'jasmine'</span><span class="p">,</span> <span class="s1">'@angular-devkit/build-angular'</span><span class="p">],</span>
<span class="na">plugins</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">require</span><span class="p">(</span><span class="s1">'karma-jasmine'</span><span class="p">),</span>
<span class="nx">require</span><span class="p">(</span><span class="s1">'karma-chrome-launcher'</span><span class="p">),</span>
<span class="nx">require</span><span class="p">(</span><span class="s1">'karma-jasmine-html-reporter'</span><span class="p">),</span>
<span class="nx">require</span><span class="p">(</span><span class="s1">'karma-coverage-istanbul-reporter'</span><span class="p">),</span>
<span class="nx">require</span><span class="p">(</span><span class="s1">'@angular-devkit/build-angular/plugins/karma'</span><span class="p">)</span>
<span class="p">],</span>
<span class="na">client</span><span class="p">:</span> <span class="p">{</span>
<span class="na">clearContext</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">},</span>
<span class="na">coverageIstanbulReporter</span><span class="p">:</span> <span class="p">{</span>
<span class="na">dir</span><span class="p">:</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'path'</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="s1">'../coverage/example'</span><span class="p">),</span>
<span class="na">reports</span><span class="p">:</span> <span class="p">[</span><span class="s1">'html'</span><span class="p">,</span> <span class="s1">'lcovonly'</span><span class="p">,</span> <span class="s1">'text-summary'</span><span class="p">],</span>
<span class="na">fixWebpackSourcePaths</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">},</span>
<span class="na">reporters</span><span class="p">:</span> <span class="p">[</span><span class="s1">'progress'</span><span class="p">,</span> <span class="s1">'kjhtml'</span><span class="p">],</span>
<span class="na">port</span><span class="p">:</span> <span class="mi">9876</span><span class="p">,</span>
<span class="na">colors</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">logLevel</span><span class="p">:</span> <span class="nx">config</span><span class="p">.</span><span class="nx">LOG_INFO</span><span class="p">,</span>
<span class="na">autoWatch</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="c1">// updated</span>
<span class="na">browsers</span><span class="p">:</span> <span class="p">[</span><span class="s1">'ChromeHeadless'</span><span class="p">],</span>
<span class="c1">// new</span>
<span class="na">customLaunchers</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'ChromeHeadless'</span><span class="p">:</span> <span class="p">{</span>
<span class="na">base</span><span class="p">:</span> <span class="s1">'Chrome'</span><span class="p">,</span>
<span class="na">flags</span><span class="p">:</span> <span class="p">[</span>
<span class="s1">'--no-sandbox'</span><span class="p">,</span>
<span class="s1">'--headless'</span><span class="p">,</span>
<span class="s1">'--disable-gpu'</span><span class="p">,</span>
<span class="s1">'--remote-debugging-port=9222'</span>
<span class="p">]</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="na">singleRun</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="na">restartOnFileChange</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p><em>e2e/protractor.conf.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">SpecReporter</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'jasmine-spec-reporter'</span><span class="p">);</span>
<span class="nx">exports</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">allScriptsTimeout</span><span class="p">:</span> <span class="mi">11000</span><span class="p">,</span>
<span class="na">specs</span><span class="p">:</span> <span class="p">[</span>
<span class="s1">'./src/**/*.e2e-spec.ts'</span>
<span class="p">],</span>
<span class="na">capabilities</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'browserName'</span><span class="p">:</span> <span class="s1">'chrome'</span><span class="p">,</span>
<span class="c1">// new</span>
<span class="s1">'chromeOptions'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'args'</span><span class="p">:</span> <span class="p">[</span>
<span class="s1">'--no-sandbox'</span><span class="p">,</span>
<span class="s1">'--headless'</span><span class="p">,</span>
<span class="s1">'--window-size=1024,768'</span>
<span class="p">]</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="na">directConnect</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">baseUrl</span><span class="p">:</span> <span class="s1">'http://localhost:4200/'</span><span class="p">,</span>
<span class="na">framework</span><span class="p">:</span> <span class="s1">'jasmine'</span><span class="p">,</span>
<span class="na">jasmineNodeOpts</span><span class="p">:</span> <span class="p">{</span>
<span class="na">showColors</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">defaultTimeoutInterval</span><span class="p">:</span> <span class="mi">30000</span><span class="p">,</span>
<span class="na">print</span><span class="p">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{}</span>
<span class="p">},</span>
<span class="nx">onPrepare</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">require</span><span class="p">(</span><span class="s1">'ts-node'</span><span class="p">).</span><span class="nx">register</span><span class="p">({</span>
<span class="na">project</span><span class="p">:</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'path'</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="s1">'./tsconfig.e2e.json'</span><span class="p">)</span>
<span class="p">});</span>
<span class="nx">jasmine</span><span class="p">.</span><span class="nx">getEnv</span><span class="p">().</span><span class="nx">addReporter</span><span class="p">(</span><span class="k">new</span> <span class="nx">SpecReporter</span><span class="p">({</span> <span class="na">spec</span><span class="p">:</span> <span class="p">{</span> <span class="na">displayStacktrace</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">}));</span>
<span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Run the unit and e2e tests:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker <span class="nb">exec</span> <span class="nt">-it</span> foo ng <span class="nb">test</span> <span class="nt">--watch</span><span class="o">=</span><span class="nb">false</span>
<span class="nv">$ </span>docker <span class="nb">exec</span> <span class="nt">-it</span> foo ng e2e <span class="nt">--port</span> 4202
</code></pre></div></div>
<p>Stop the container once done:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker stop foo
</code></pre></div></div>
<p>Want to use <a href="https://docs.docker.com/compose/">Docker Compose</a>? Add a <em>docker-compose.yml</em> file to the project root:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.7'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">example</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">example</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">.:/app'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">/app/node_modules'</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">4201:4200'</span>
</code></pre></div></div>
<p>Take note of the volumes. Without the <a href="https://success.docker.com/article/Different_Types_of_Volumes">anonymous</a> volume (<code class="highlighter-rouge">'/app/node_modules'</code>), the <em>node_modules</em> directory would be overwritten by the mounting of the host directory at runtime. In other words, this would happen:</p>
<ul>
<li><em>Build</em> - The <code class="highlighter-rouge">node_modules</code> directory is created in the image.</li>
<li><em>Run</em> - The current directory is mounted into the container, overwriting the <code class="highlighter-rouge">node_modules</code> that were installed during the build.</li>
</ul>
<p>Build the image and fire up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">-d</span> <span class="nt">--build</span>
</code></pre></div></div>
<p>Ensure the app is running in the browser and test hot-reloading again. Try both the unit and e2e tests as well:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose <span class="nb">exec </span>example ng <span class="nb">test</span> <span class="nt">--watch</span><span class="o">=</span><span class="nb">false</span>
<span class="nv">$ </span>docker-compose <span class="nb">exec </span>example ng e2e <span class="nt">--port</span> 4202
</code></pre></div></div>
<p>Stop the container before moving on:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose stop
</code></pre></div></div>
<blockquote>
<p><em>Windows Users</em>: Having problems getting the volumes to work properly? Review the following resources:</p>
<ol>
<li><a href="https://rominirani.com/docker-on-windows-mounting-host-directories-d96f3f056a2c">Docker on Windows–Mounting Host Directories</a></li>
<li><a href="https://blogs.msdn.microsoft.com/stevelasker/2016/06/14/configuring-docker-for-windows-volumes/">Configuring Docker for Windows Shared Drives</a></li>
</ol>
<p>You also may need to add <code class="highlighter-rouge">COMPOSE_CONVERT_WINDOWS_PATHS=1</code> to the environment portion of your Docker Compose file. Review the <a href="https://docs.docker.com/compose/env-file/">Declare default environment variables in file</a> guide for more info.</p>
</blockquote>
<h2 id="docker-machine">Docker Machine</h2>
<p>To get hot-reloading to work with <a href="https://docs.docker.com/machine/">Docker Machine</a> and <a href="https://docs.docker.com/machine/get-started/">VirtualBox</a> you’ll need to enable a polling mechanism via <a href="https://github.com/paulmillr/chokidar">chokidar</a> (which wraps <code class="highlighter-rouge">fs.watch</code>, <code class="highlighter-rouge">fs.watchFile</code>, and <code class="highlighter-rouge">fsevents</code>).</p>
<p>Create a new Machine:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-machine create <span class="nt">-d</span> virtualbox clever
<span class="nv">$ </span>docker-machine env clever
<span class="nv">$ </span><span class="nb">eval</span> <span class="k">$(</span>docker-machine env clever<span class="k">)</span>
</code></pre></div></div>
<p>Grab the IP address:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-machine ip clever
</code></pre></div></div>
<p>Then, build the images:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker build <span class="nt">-t</span> example:dev <span class="nb">.</span>
</code></pre></div></div>
<p>And run the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-it</span> <span class="nt">-v</span> <span class="k">${</span><span class="nv">PWD</span><span class="k">}</span>:/app <span class="nt">-v</span> /app/node_modules <span class="nt">-p</span> 4201:4200 <span class="nt">--rm</span> example:dev
</code></pre></div></div>
<p>Test the app again in the browser at <a href="http://DOCKER_MACHINE_IP:4201">http://DOCKER_MACHINE_IP:4201</a> (make sure to replace <code class="highlighter-rouge">DOCKER_MACHINE_IP</code> with the actual IP address of the Docker Machine). Also, confirm that auto reload is <em>not</em> working. You can try with Docker Compose as well, but the result will be the same.</p>
<p>To get hot-reload working, we need to add an environment variable: <code class="highlighter-rouge">CHOKIDAR_USEPOLLING=true</code>.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-it</span> <span class="nt">-v</span> <span class="k">${</span><span class="nv">PWD</span><span class="k">}</span>:/app <span class="nt">-v</span> /app/node_modules <span class="nt">-p</span> 4201:4200 <span class="nt">-e</span> <span class="nv">CHOKIDAR_USEPOLLING</span><span class="o">=</span><span class="nb">true</span> <span class="nt">--rm</span> example:dev
</code></pre></div></div>
<p>Test it out again. Then, kill the server and add the environment variable to the <em>docker-compose.yml</em> file:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.7'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">example</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">example</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">.:/app'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">/app/node_modules'</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">4201:4200'</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">CHOKIDAR_USEPOLLING=true</span>
</code></pre></div></div>
<p>Spin up the container. Run the unit tests and e2e tests.</p>
<h2 id="production">Production</h2>
<p>Let’s create a separate Dockerfile for use in production called <em>Dockerfile-prod</em>:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#############</span>
<span class="c">### build ###</span>
<span class="c">#############</span>
<span class="c"># base image</span>
<span class="k">FROM</span><span class="s"> node:12.2.0 as build</span>
<span class="c"># install chrome for protractor tests</span>
<span class="k">RUN </span>wget <span class="nt">-q</span> <span class="nt">-O</span> - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
<span class="k">RUN </span>sh <span class="nt">-c</span> <span class="s1">'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'</span>
<span class="k">RUN </span>apt-get update <span class="o">&&</span> apt-get install <span class="nt">-yq</span> google-chrome-stable
<span class="c"># set working directory</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="c"># add `/app/node_modules/.bin` to $PATH</span>
<span class="k">ENV</span><span class="s"> PATH /app/node_modules/.bin:$PATH</span>
<span class="c"># install and cache app dependencies</span>
<span class="k">COPY</span><span class="s"> package.json /app/package.json</span>
<span class="k">RUN </span>npm install
<span class="k">RUN </span>npm install <span class="nt">-g</span> @angular/cli@7.3.9
<span class="c"># add app</span>
<span class="k">COPY</span><span class="s"> . /app</span>
<span class="c"># run tests</span>
<span class="k">RUN </span>ng <span class="nb">test</span> <span class="nt">--watch</span><span class="o">=</span><span class="nb">false</span>
<span class="k">RUN </span>ng e2e <span class="nt">--port</span> 4202
<span class="c"># generate build</span>
<span class="k">RUN </span>ng build <span class="nt">--output-path</span><span class="o">=</span>dist
<span class="c">############</span>
<span class="c">### prod ###</span>
<span class="c">############</span>
<span class="c"># base image</span>
<span class="k">FROM</span><span class="s"> nginx:1.16.0-alpine</span>
<span class="c"># copy artifact build from the 'build environment'</span>
<span class="k">COPY</span><span class="s"> --from=build /app/dist /usr/share/nginx/html</span>
<span class="c"># expose port 80</span>
<span class="k">EXPOSE</span><span class="s"> 80</span>
<span class="c"># run nginx</span>
<span class="k">CMD</span><span class="s"> ["nginx", "-g", "daemon off;"]</span>
</code></pre></div></div>
<p>Two important things to note:</p>
<ol>
<li>
<p>First, we take advantage of the <a href="https://docs.docker.com/engine/userguide/eng-image/multistage-build/">multistage build</a> pattern to create a temporary image used for building the artifact – the production-ready Angular static files – that is then copied over to the production image. The temporary build image is discarded along with the original files, folders, and dependencies associated with the image. This produces a lean, production-ready image.</p>
<p>In other words, the only thing kept from the first image is the compiled distribution code.</p>
<blockquote>
<p>Check out the <a href="https://blog.alexellis.io/mutli-stage-docker-builds/">Builder pattern vs. Multi-stage builds in Docker</a> blog post for more info on multistage builds.</p>
</blockquote>
</li>
<li>
<p>Next, the unit and e2e tests are run in the build process, so the build will fail if the tests do not succeed.</p>
</li>
</ol>
<p>Using the production Dockerfile, build and tag the Docker image:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker build <span class="nt">-f</span> Dockerfile-prod <span class="nt">-t</span> example:prod <span class="nb">.</span>
</code></pre></div></div>
<p>Spin up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">-it</span> <span class="nt">-p</span> 80:80 <span class="nt">--rm</span> example:prod
</code></pre></div></div>
<p>Assuming you are still using the same Docker Machine, navigate to <a href="http://DOCKER_MACHINE_IP/">http://DOCKER_MACHINE_IP/</a> in your browser.</p>
<p>Test with a new Docker Compose file as well called <em>docker-compose-prod.yml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.7'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">example-prod</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">example-prod</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile-prod</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">80:80'</span>
</code></pre></div></div>
<p>Fire up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose <span class="nt">-f</span> docker-compose-prod.yml up <span class="nt">-d</span> <span class="nt">--build</span>
</code></pre></div></div>
<p>Test it out once more in your browser. Then, break a test in <em>src/app/app.component.spec.ts</em> and re-build. It should fail:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ERROR: Service <span class="s1">'example-prod'</span> failed to build:
The <span class="nb">command</span> <span class="s1">'/bin/sh -c ng test --watch=false'</span> returned a non-zero code: 1
</code></pre></div></div>
<p>If you’re done, go ahead and destroy the Machine:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">eval</span> <span class="k">$(</span>docker-machine env <span class="nt">-u</span><span class="k">)</span>
<span class="nv">$ </span>docker-machine rm clever
</code></pre></div></div>
<hr />
<p>Cheers!</p>
Michael Herman
Docker is a containerization tool used to streamline application development and deployment workflows across various environments.
Node, gRPC, and Postgres
2019-02-01T00:00:00-06:00
2019-02-01T00:00:00-06:00
https://mherman.org/blog/node-grpc-postgres
<p><a href="https://grpc.io/">gRPC</a> is an open source <a href="https://en.wikipedia.org/wiki/Remote_procedure_call">remote procedure call</a> (RPC) framework developed by Google. It’s built on top of <a href="https://en.wikipedia.org/wiki/HTTP/2">HTTP/2</a>, and it uses <a href="https://developers.google.com/protocol-buffers/">Protocol Buffers</a> as the underlying data serialization format.</p>
<p>This tutorial looks at how to implement an API with Node, gRPC, and Postgres.</p>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#learning-objectives" id="markdown-toc-learning-objectives">Learning Objectives</a></li>
<li><a href="#grpc" id="markdown-toc-grpc">gRPC</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#grpc-service" id="markdown-toc-grpc-service">gRPC Service</a></li>
<li><a href="#server" id="markdown-toc-server">Server</a></li>
<li><a href="#client" id="markdown-toc-client">Client</a></li>
<li><a href="#crud" id="markdown-toc-crud">CRUD</a></li>
<li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>
<h2 id="learning-objectives">Learning Objectives</h2>
<ol>
<li>Explain what gRPC is and how it compares to REST</li>
<li>Define the service definition and payload message structure using a Protocol Buffer</li>
<li>Describe the four types of methods–unary, client streaming, server streaming, and bidirectional streaming</li>
<li>Create a gRPC server and client in Node</li>
<li>Invoke the gRPC server from the client via the gRPC stubs</li>
<li>Describe the static and dynamic approaches to Protocol Buffers in Node</li>
</ol>
<h2 id="grpc">gRPC</h2>
<p>As mentioned, gRPC is an RPC framework that leverages HTTP/2 and Protocol Buffers, making it a fast, secure, and reliable communication protocol for microservices.</p>
<blockquote>
<p>New to gRPC? Start with the official <a href="https://grpc.io/docs/">What is gRPC?</a> guide.</p>
</blockquote>
<p><em>So, how does gRPC compare to REST?</em></p>
<p>We’ll use the most common implementation of REST for this comparison: An API that uses the HTTP verbs–GET, POST, PUT, DELETE–to accept and return JSON.</p>
<ol>
<li><em>Speed</em>: HTTP/2 supports bidirectional streams (and flow control), so once a connection is established the client or server can push and pull data. Check out this <a href="http://www.http2demo.io/">demo</a> of HTTP/1.1 vs. HTTP/2 for a performance comparison. Also, you can read all about the features in HTTP/2 <a href="https://http2.github.io/faq/">here</a>.</li>
<li><em>Messages</em> (instead of resources and verbs): With gRPC you are no longer constrained to a limited set of methods. You can create your own methods, like <code class="highlighter-rouge">getCustomerList</code> or <code class="highlighter-rouge">emailCustomerOrderReceipt</code>, that can be leveraged by simply calling the method. And, from a developer’s perspective, calling a gRPC method looks and feels just like calling any other native local method–it’s just making a network call beneath the scenes.</li>
<li><em>Protocol Buffers</em>: gRPC uses Protocol Buffers (strongly typed, binary-based) rather than JSON (loosely typed, text-based), which is much more efficient and introduces type safety. The service definitions themselves are defined declaratively and are used to generate client libraries. Further, Protocol Buffers, when compared to JSON, can decrease the size of the payloads being transmitted, which, at scale, can reduce bandwidth cost.</li>
</ol>
<blockquote>
<p>For more on gRPC vs REST, review the <a href="https://code.tutsplus.com/tutorials/rest-vs-grpc-battle-of-the-apis--cms-30711">REST vs. gRPC: Battle of the APIs</a> blog post.</p>
</blockquote>
<h2 id="project-setup">Project Setup</h2>
<p>Create a new directory to hold the project:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>mkdir node-grpc-crud
<span class="nv">$ </span><span class="nb">cd </span>node-grpc-crud
</code></pre></div></div>
<blockquote>
<p>If you don’t want to code along, you can find the <a href="https://github.com/mjhea0/node-grpc-crud">final code on GitHub</a>.</p>
</blockquote>
<p>Then, add the following files and folders:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── client
│ ├── app.js
│ └── package.json
├── protos
└── server
├── index.js
└── package.json
</code></pre></div></div>
<p>Finally, spin up <a href="https://www.postgresql.org/">PostgreSQL</a> on port 5432 and create a new database called <code class="highlighter-rouge">grpc_products</code>.</p>
<h2 id="grpc-service">gRPC Service</h2>
<p>Throughout this tutorial, you’ll be building a product API backed by gRPC that handles the basic CRUD functionality.</p>
<p>Let’s start by <a href="https://developers.google.com/protocol-buffers/docs/proto3#services">implementing a gRPC Service</a>, which is defined by the methods it exposes along with it’s associated parameters and return message types.</p>
<p>Add a new file to the “protos” folder called <em>product.proto</em> to hold the gRPC service definition and message type definitions:</p>
<div class="language-protobuf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">syntax</span> <span class="o">=</span> <span class="s">"proto3"</span><span class="p">;</span>
<span class="kn">package</span> <span class="nn">product</span><span class="p">;</span>
<span class="c1">// service definition
</span>
<span class="kd">service</span> <span class="n">ProductService</span> <span class="p">{</span>
<span class="k">rpc</span> <span class="n">listProducts</span><span class="p">(</span><span class="n">Empty</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">ProductList</span><span class="p">)</span> <span class="p">{}</span>
<span class="k">rpc</span> <span class="n">readProduct</span><span class="p">(</span><span class="n">ProductId</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">Product</span><span class="p">)</span> <span class="p">{}</span>
<span class="k">rpc</span> <span class="n">createProduct</span><span class="p">(</span><span class="n">newProduct</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="p">{}</span>
<span class="k">rpc</span> <span class="n">updateProduct</span><span class="p">(</span><span class="n">Product</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="p">{}</span>
<span class="k">rpc</span> <span class="n">deleteProduct</span><span class="p">(</span><span class="n">ProductId</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="c1">// message type definitions
</span>
<span class="kd">message</span> <span class="nc">Empty</span> <span class="p">{}</span>
<span class="kd">message</span> <span class="nc">ProductList</span> <span class="p">{</span>
<span class="k">repeated</span> <span class="n">Product</span> <span class="na">products</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">message</span> <span class="nc">ProductId</span> <span class="p">{</span>
<span class="kt">int32</span> <span class="na">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">message</span> <span class="nc">Product</span> <span class="p">{</span>
<span class="kt">int32</span> <span class="na">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="kt">string</span> <span class="na">name</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="kt">string</span> <span class="na">price</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">message</span> <span class="nc">newProduct</span> <span class="p">{</span>
<span class="kt">string</span> <span class="na">name</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="kt">string</span> <span class="na">price</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">message</span> <span class="nc">result</span> <span class="p">{</span>
<span class="kt">string</span> <span class="na">status</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<blockquote>
<p>Using VSCode? Check out the <a href="https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3">proto3 vscode extension</a>.</p>
</blockquote>
<p>First, we used the <a href="https://developers.google.com/protocol-buffers/docs/proto3">proto3</a> version of the Protocol Buffer language:</p>
<div class="language-protobuf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">syntax</span> <span class="o">=</span> <span class="s">"proto3"</span><span class="p">;</span>
</code></pre></div></div>
<p>Then, we defined a <a href="https://developers.google.com/protocol-buffers/docs/proto3#packages">package specifier</a>:</p>
<div class="language-protobuf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">product</span><span class="p">;</span>
</code></pre></div></div>
<blockquote>
<p>The package specifier is optional but it’s generally a good idea to use to avoid name clashes. Review the <a href="https://developers.google.com/protocol-buffers/docs/reference/javascript-generated#package">JavaScript Generated Code</a> reference guide for more info.</p>
</blockquote>
<p>Next, we defined five RPC methods that align to the basic CRUD operations:</p>
<table>
<thead>
<tr>
<th>CRUD</th>
<th>gRPC method</th>
</tr>
</thead>
<tbody>
<tr>
<td>Read</td>
<td>listProducts</td>
</tr>
<tr>
<td>Create</td>
<td>createProduct</td>
</tr>
<tr>
<td>Read</td>
<td>readProduct</td>
</tr>
<tr>
<td>Update</td>
<td>updateProduct</td>
</tr>
<tr>
<td>Delete</td>
<td>deleteProduct</td>
</tr>
</tbody>
</table>
<p>There are <a href="https://grpc.io/docs/guides/concepts.html#rpc-life-cycle">four types of methods</a> in gRPC-land:</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Unary</td>
<td>client sends a single request and gets a single response</td>
<td><code class="highlighter-rouge">rpc getData(req) returns (rsp) {}</code></td>
</tr>
<tr>
<td>Server streaming</td>
<td>client sends a single request and gets back a stream</td>
<td><code class="highlighter-rouge">rpc getData(req) returns (stream rsp) {}</code></td>
</tr>
<tr>
<td>Client streaming</td>
<td>client sends stream and gets back a single response</td>
<td><code class="highlighter-rouge">rpc getData(stream req) returns (rsp) {}</code></td>
</tr>
<tr>
<td>Bidirectional streaming</td>
<td>both the client and server send and receive streams</td>
<td><code class="highlighter-rouge">rpc getData(stream req) returns (stream rsp) {}</code></td>
</tr>
</tbody>
</table>
<p>Our example uses the Unary approach.</p>
<p>The first method, <code class="highlighter-rouge">listProducts</code>, takes an <code class="highlighter-rouge">Empty</code> message and returns a <code class="highlighter-rouge">ProductList</code> message, which has a repeated <code class="highlighter-rouge">Product</code> field called <code class="highlighter-rouge">products</code>:</p>
<div class="language-protobuf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">message</span> <span class="nc">ProductList</span> <span class="p">{</span>
<span class="k">repeated</span> <span class="n">Product</span> <span class="na">products</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<blockquote>
<p>The <a href="https://developers.google.com/protocol-buffers/docs/proto3#specifying-field-rules">repeated</a> keyword is used to define an array of objects.</p>
</blockquote>
<p>We also created an <code class="highlighter-rouge">Empty</code> message as the empty stub for empty requests and responses:</p>
<div class="language-protobuf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">message</span> <span class="nc">Empty</span> <span class="p">{}</span>
</code></pre></div></div>
<p>The next method, <code class="highlighter-rouge">readProduct</code>, takes a <code class="highlighter-rouge">ProductId</code> and returns a <code class="highlighter-rouge">Product</code>:</p>
<div class="language-protobuf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">message</span> <span class="nc">Product</span> <span class="p">{</span>
<span class="kt">int32</span> <span class="na">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="kt">string</span> <span class="na">name</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="kt">string</span> <span class="na">price</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This has three <a href="https://developers.google.com/protocol-buffers/docs/proto3#scalar">scalar value fields</a>, each with a <a href="https://developers.google.com/protocol-buffers/docs/proto3#assigning-field-numbers">unique numbered tag</a>:</p>
<ol>
<li>id (int32)</li>
<li>name (string)</li>
<li>price (string)</li>
</ol>
<p>We created a <code class="highlighter-rouge">ProductId</code> as well with a single int32 field:</p>
<div class="language-protobuf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">message</span> <span class="nc">ProductId</span> <span class="p">{</span>
<span class="kt">int32</span> <span class="na">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Next, <code class="highlighter-rouge">createProduct</code> takes a <code class="highlighter-rouge">newProduct</code> and returns a <code class="highlighter-rouge">result</code>, which will be just a status message of “success” or “failure”. Take note of the <code class="highlighter-rouge">newProduct</code> message and the <code class="highlighter-rouge">updateProduct</code> and <code class="highlighter-rouge">deleteProduct</code> methods on your own.</p>
<p>With the service defined, you can now generate interfaces in a number of different languages.</p>
<h2 id="server">Server</h2>
<p>Moving on, let’s create the gRPC server that will be used to serve up the remote procedure calls.</p>
<h3 id="setup">Setup</h3>
<p>Start by updating the <em>package.json</em> file in the “server” folder:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node-grpc-server"</span><span class="p">,</span><span class="w">
</span><span class="s2">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"@grpc/proto-loader"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^0.4.0"</span><span class="p">,</span><span class="w">
</span><span class="s2">"google-protobuf"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^3.6.1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"grpc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^1.18.0"</span><span class="p">,</span><span class="w">
</span><span class="s2">"knex"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^0.16.3"</span><span class="p">,</span><span class="w">
</span><span class="s2">"pg"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^7.8.0"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"start"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node index.js"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Take note of the dependencies:</p>
<ol>
<li><a href="https://www.npmjs.com/package/@grpc/proto-loader">@grpc/proto-loader</a>: loads <em>.proto</em> files</li>
<li><a href="https://www.npmjs.com/package/google-protobuf">google-protobuf</a>: JavaScript version of the Protocol Buffers runtime library</li>
<li><a href="https://www.npmjs.com/package/grpc">grpc</a>: Node gRPC Library</li>
<li><a href="https://www.npmjs.com/package/knex">knex</a>: SQL query builder for Node</li>
<li><a href="https://www.npmjs.com/package/pg">pg</a>: PostgreSQL client for Node</li>
</ol>
<p>Before installing, you will need to install <code class="highlighter-rouge">protoc</code>, the Protobuf Compiler. If you’re on a Mac, it’s easiest to install with <a href="https://brew.sh/">Homebrew</a>:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>brew install protobuf
</code></pre></div></div>
<p>Otherwise, you can manually download and install it from <a href="https://github.com/protocolbuffers/protobuf/releases/tag/v3.6.1">here</a>.</p>
<p>Now you can install the NPM dependencies:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cd </span>server
<span class="nv">$ </span>npm install
</code></pre></div></div>
<p>Next, add a <em>knexfile.js</em> file to the “server” folder:, which is used for configuring knex for different environments–e.g., local, development, production:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'path'</span><span class="p">);</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">development</span><span class="p">:</span> <span class="p">{</span>
<span class="na">client</span><span class="p">:</span> <span class="s1">'postgresql'</span><span class="p">,</span>
<span class="na">connection</span><span class="p">:</span> <span class="p">{</span>
<span class="na">host</span><span class="p">:</span> <span class="s1">'127.0.0.1'</span><span class="p">,</span>
<span class="na">user</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="na">port</span><span class="p">:</span> <span class="s1">'5432'</span><span class="p">,</span>
<span class="na">database</span><span class="p">:</span> <span class="s1">'grpc_products'</span><span class="p">,</span>
<span class="p">},</span>
<span class="na">pool</span><span class="p">:</span> <span class="p">{</span>
<span class="na">min</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="na">max</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
<span class="p">},</span>
<span class="na">migrations</span><span class="p">:</span> <span class="p">{</span>
<span class="na">directory</span><span class="p">:</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="s1">'db'</span><span class="p">,</span> <span class="s1">'migrations'</span><span class="p">),</span>
<span class="p">},</span>
<span class="na">seeds</span><span class="p">:</span> <span class="p">{</span>
<span class="na">directory</span><span class="p">:</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="s1">'db'</span><span class="p">,</span> <span class="s1">'seeds'</span><span class="p">),</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Add the appropriate username and password. Then, create the a “db” directory along with the “migrations” and “seeds” directories. Your project structure should now look like:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── client
│ ├── app.js
│ └── package.json
├── protos
│ └── product.proto
└── server
├── db
│ ├── migrations
│ └── seeds
├── index.js
├── knexfile.js
├── package-lock.json
└── package.json
</code></pre></div></div>
<p>Create a new migration file:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./node_modules/.bin/knex migrate:make products
</code></pre></div></div>
<p>Then, add the following code to the migration stub in “server/db/migrations”:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">exports</span><span class="p">.</span><span class="nx">up</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">knex</span><span class="p">,</span> <span class="nb">Promise</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">createTable</span><span class="p">(</span><span class="s1">'products'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">table</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">table</span><span class="p">.</span><span class="nx">increments</span><span class="p">();</span>
<span class="nx">table</span><span class="p">.</span><span class="nx">string</span><span class="p">(</span><span class="s1">'name'</span><span class="p">).</span><span class="nx">notNullable</span><span class="p">();</span>
<span class="nx">table</span><span class="p">.</span><span class="nx">string</span><span class="p">(</span><span class="s1">'price'</span><span class="p">).</span><span class="nx">notNullable</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="nx">exports</span><span class="p">.</span><span class="nx">down</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">knex</span><span class="p">,</span> <span class="nb">Promise</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">dropTable</span><span class="p">(</span><span class="s1">'products'</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Apply the migration:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./node_modules/.bin/knex migrate:latest
</code></pre></div></div>
<p>Create a new seed file:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./node_modules/.bin/knex seed:make products
</code></pre></div></div>
<p>Add the code:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">exports</span><span class="p">.</span><span class="nx">seed</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">knex</span><span class="p">,</span> <span class="nb">Promise</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// Deletes ALL existing entries</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'products'</span><span class="p">).</span><span class="nx">del</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="c1">// Inserts seed entries</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'products'</span><span class="p">).</span><span class="nx">insert</span><span class="p">([</span>
<span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="s1">'pencil'</span><span class="p">,</span> <span class="na">price</span><span class="p">:</span> <span class="s1">'1.99'</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="s1">'pen'</span><span class="p">,</span> <span class="na">price</span><span class="p">:</span> <span class="s1">'2.99'</span> <span class="p">},</span>
<span class="p">]);</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Apply the seed:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./node_modules/.bin/knex seed:run
</code></pre></div></div>
<p>Finally, wire up the basic server in <em>index.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// requirements</span>
<span class="kd">const</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'path'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">protoLoader</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'@grpc/proto-loader'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">grpc</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'grpc'</span><span class="p">);</span>
<span class="c1">// knex</span>
<span class="kd">const</span> <span class="nx">environment</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">ENVIRONMENT</span> <span class="o">||</span> <span class="s1">'development'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">config</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./knexfile.js'</span><span class="p">)[</span><span class="nx">environment</span><span class="p">];</span>
<span class="kd">const</span> <span class="nx">knex</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'knex'</span><span class="p">)(</span><span class="nx">config</span><span class="p">);</span>
<span class="c1">// grpc service definition</span>
<span class="kd">const</span> <span class="nx">productProtoPath</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="s1">'..'</span><span class="p">,</span> <span class="s1">'protos'</span><span class="p">,</span> <span class="s1">'product.proto'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">productProtoDefinition</span> <span class="o">=</span> <span class="nx">protoLoader</span><span class="p">.</span><span class="nx">loadSync</span><span class="p">(</span><span class="nx">productProtoPath</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">productPackageDefinition</span> <span class="o">=</span> <span class="nx">grpc</span><span class="p">.</span><span class="nx">loadPackageDefinition</span><span class="p">(</span><span class="nx">productProtoDefinition</span><span class="p">).</span><span class="nx">product</span><span class="p">;</span>
<span class="cm">/*
Using an older version of gRPC?
(1) You won't need the @grpc/proto-loader package
(2) const productPackageDefinition = grpc.load(productProtoPath).product;
*/</span>
<span class="c1">// knex queries</span>
<span class="kd">function</span> <span class="nx">listProducts</span><span class="p">(</span><span class="nx">call</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{}</span>
<span class="kd">function</span> <span class="nx">readProduct</span><span class="p">(</span><span class="nx">call</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{}</span>
<span class="kd">function</span> <span class="nx">createProduct</span><span class="p">(</span><span class="nx">call</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{}</span>
<span class="kd">function</span> <span class="nx">updateProduct</span><span class="p">(</span><span class="nx">call</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{}</span>
<span class="kd">function</span> <span class="nx">deleteProduct</span><span class="p">(</span><span class="nx">call</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{}</span>
<span class="c1">// main</span>
<span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">grpc</span><span class="p">.</span><span class="nx">Server</span><span class="p">();</span>
<span class="c1">// gRPC service</span>
<span class="nx">server</span><span class="p">.</span><span class="nx">addService</span><span class="p">(</span><span class="nx">productPackageDefinition</span><span class="p">.</span><span class="nx">ProductService</span><span class="p">.</span><span class="nx">service</span><span class="p">,</span> <span class="p">{</span>
<span class="na">listProducts</span><span class="p">:</span> <span class="nx">listProducts</span><span class="p">,</span>
<span class="na">readProduct</span><span class="p">:</span> <span class="nx">readProduct</span><span class="p">,</span>
<span class="na">createProduct</span><span class="p">:</span> <span class="nx">createProduct</span><span class="p">,</span>
<span class="na">updateProduct</span><span class="p">:</span> <span class="nx">updateProduct</span><span class="p">,</span>
<span class="na">deleteProduct</span><span class="p">:</span> <span class="nx">deleteProduct</span><span class="p">,</span>
<span class="p">});</span>
<span class="c1">// gRPC server</span>
<span class="nx">server</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="s1">'localhost:50051'</span><span class="p">,</span> <span class="nx">grpc</span><span class="p">.</span><span class="nx">ServerCredentials</span><span class="p">.</span><span class="nx">createInsecure</span><span class="p">());</span>
<span class="nx">server</span><span class="p">.</span><span class="nx">start</span><span class="p">();</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'gRPC server running at http://127.0.0.1:50051'</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">main</span><span class="p">();</span>
</code></pre></div></div>
<p>Here, we load the gRPC service definition, create a new gRPC server, add the service to the server, and then run the server on <a href="http://localhost:50051">http://localhost:50051</a>.</p>
<p>Take note of the <code class="highlighter-rouge">addService</code> method:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">server</span><span class="p">.</span><span class="nx">addService</span><span class="p">(</span><span class="nx">productPackageDefinition</span><span class="p">.</span><span class="nx">ProductService</span><span class="p">.</span><span class="nx">service</span><span class="p">,</span> <span class="p">{</span>
<span class="na">listProducts</span><span class="p">:</span> <span class="nx">listProducts</span><span class="p">,</span>
<span class="na">readProduct</span><span class="p">:</span> <span class="nx">readProduct</span><span class="p">,</span>
<span class="na">createProduct</span><span class="p">:</span> <span class="nx">createProduct</span><span class="p">,</span>
<span class="na">updateProduct</span><span class="p">:</span> <span class="nx">updateProduct</span><span class="p">,</span>
<span class="na">deleteProduct</span><span class="p">:</span> <span class="nx">deleteProduct</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>
<p>This method adds the gRPC service, defined in the <em>product.proto</em>, to the server. It takes the service definition–e.g., <code class="highlighter-rouge">ProductService</code>–along with an object which maps the method names from the gRPC service to the method implementations defined above.</p>
<h3 id="dynamic-vs-static-code-generation">Dynamic vs Static Code Generation</h3>
<p>There are two ways of working with Protocol Buffers in Node:</p>
<ol>
<li><em>Dynamically</em>: with dynamic code generation, the Protocol Buffer is loaded and parsed at run time with <a href="https://github.com/dcodeIO/ProtoBuf.js/">Protobuf.js</a></li>
<li><em>Statically</em>: with the static approach, the Protocol Buffer is pre-processed into JavaScript</li>
</ol>
<p>We used the dynamic approach above. The dynamic approach is quite a bit simpler to implement, but it differs from the workflow of other gRPC-supported languages, since they require static code generation.</p>
<p>Want to use the static approach?</p>
<p>First, install <a href="https://github.com/grpc/grpc-node#grpc-tools">grpc-tools</a> globally:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install <span class="nt">-g</span> grpc-tools
</code></pre></div></div>
<p>Then, run the following from the project root:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>protoc <span class="nt">-I</span><span class="o">=</span><span class="nb">.</span> ./protos/product.proto <span class="se">\</span>
<span class="nt">--js_out</span><span class="o">=</span><span class="nv">import_style</span><span class="o">=</span>commonjs,binary:./server <span class="se">\</span>
<span class="nt">--grpc_out</span><span class="o">=</span>./server <span class="se">\</span>
<span class="nt">--plugin</span><span class="o">=</span>protoc-gen-grpc<span class="o">=</span><span class="sb">`</span>which grpc_tools_node_protoc_plugin<span class="sb">`</span>
</code></pre></div></div>
<p>The generated code should now be in the “server/protos” directory:</p>
<ol>
<li><em>product_grpc_pb.js</em></li>
<li><em>product_pb.js</em></li>
</ol>
<p>You’ll then import the generated code into your <em>index.js</em> file and use them directly rather than loading the service definition.</p>
<h3 id="sanity-check">Sanity Check</h3>
<p>Try running the server at this point:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm start
</code></pre></div></div>
<p>You should see:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gRPC server running at http://127.0.0.1:50051
</code></pre></div></div>
<p>Let’s wire up the gRPC client before adding the knex query functions.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── client
│ ├── app.js
│ └── package.json
├── protos
│ └── product.proto
└── server
├── db
│ ├── migrations
│ │ └── 20190131084532_products.js
│ └── seeds
│ └── products.js
├── index.js
├── knexfile.js
├── package-lock.json
└── package.json
</code></pre></div></div>
<h2 id="client">Client</h2>
<p>Like before, update the <em>package.json</em> file the “client” directory:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node-grpc-client"</span><span class="p">,</span><span class="w">
</span><span class="s2">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"@grpc/proto-loader"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^0.4.0"</span><span class="p">,</span><span class="w">
</span><span class="s2">"body-parser"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^1.18.3"</span><span class="p">,</span><span class="w">
</span><span class="s2">"express"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.16.4"</span><span class="p">,</span><span class="w">
</span><span class="s2">"google-protobuf"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^3.6.1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"grpc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^1.18.0"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"start"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node app.js"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>From the “client” directory, install the dependencies:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install
</code></pre></div></div>
<p>Update <em>app.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// requirements</span>
<span class="kd">const</span> <span class="nx">express</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'express'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">bodyParser</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'body-parser'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">productRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./routes/productRoutes'</span><span class="p">);</span>
<span class="c1">// express</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">bodyParser</span><span class="p">.</span><span class="nx">urlencoded</span><span class="p">({</span> <span class="na">extended</span><span class="p">:</span> <span class="kc">false</span> <span class="p">}));</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">bodyParser</span><span class="p">.</span><span class="nx">json</span><span class="p">());</span>
<span class="c1">// routes</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="s1">'/api'</span><span class="p">,</span> <span class="nx">productRoutes</span><span class="p">);</span>
<span class="c1">// run server</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="mi">3000</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Server listing on port 3000'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Here, we instantiate a new Express server, define our routes, and then run the server. Add a “routes” folder along with the <a href="https://expressjs.com/en/api.html#router">Express router</a> in a <em>productRoutes.js</em> file:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// requirements</span>
<span class="kd">const</span> <span class="nx">express</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'express'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">grpcRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./grpcRoutes'</span><span class="p">);</span>
<span class="c1">// new router</span>
<span class="kd">const</span> <span class="nx">router</span> <span class="o">=</span> <span class="nx">express</span><span class="p">.</span><span class="nx">Router</span><span class="p">();</span>
<span class="c1">// routes</span>
<span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/products'</span><span class="p">,</span> <span class="nx">grpcRoutes</span><span class="p">.</span><span class="nx">listProducts</span><span class="p">);</span>
<span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/products/:id'</span><span class="p">,</span> <span class="nx">grpcRoutes</span><span class="p">.</span><span class="nx">readProduct</span><span class="p">);</span>
<span class="nx">router</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'/products'</span><span class="p">,</span> <span class="nx">grpcRoutes</span><span class="p">.</span><span class="nx">createProduct</span><span class="p">);</span>
<span class="nx">router</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="s1">'/products/:id'</span><span class="p">,</span> <span class="nx">grpcRoutes</span><span class="p">.</span><span class="nx">updateProduct</span><span class="p">);</span>
<span class="nx">router</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="s1">'/products/:id'</span><span class="p">,</span> <span class="nx">grpcRoutes</span><span class="p">.</span><span class="nx">deleteProduct</span><span class="p">);</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">router</span><span class="p">;</span>
</code></pre></div></div>
<p>Finally, add the boilerplate for the gRPC client in <em>client/routes/grpcRoutes.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// requirements</span>
<span class="kd">const</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'path'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">protoLoader</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'@grpc/proto-loader'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">grpc</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'grpc'</span><span class="p">);</span>
<span class="c1">// gRPC client</span>
<span class="kd">const</span> <span class="nx">productProtoPath</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="s1">'..'</span><span class="p">,</span> <span class="s1">'..'</span><span class="p">,</span> <span class="s1">'protos'</span><span class="p">,</span> <span class="s1">'product.proto'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">productProtoDefinition</span> <span class="o">=</span> <span class="nx">protoLoader</span><span class="p">.</span><span class="nx">loadSync</span><span class="p">(</span><span class="nx">productProtoPath</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">productPackageDefinition</span> <span class="o">=</span> <span class="nx">grpc</span><span class="p">.</span><span class="nx">loadPackageDefinition</span><span class="p">(</span><span class="nx">productProtoDefinition</span><span class="p">).</span><span class="nx">product</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">productPackageDefinition</span><span class="p">.</span><span class="nx">ProductService</span><span class="p">(</span>
<span class="s1">'localhost:50051'</span><span class="p">,</span> <span class="nx">grpc</span><span class="p">.</span><span class="nx">credentials</span><span class="p">.</span><span class="nx">createInsecure</span><span class="p">());</span>
<span class="cm">/*
Using an older version of gRPC?
(1) You won't need the @grpc/proto-loader package
(2) const productPackageDefinition = grpc.load(productProtoPath).product;
(3) const client = new productPackageDefinition.ProductService(
'localhost:50051', grpc.credentials.createInsecure());
*/</span>
<span class="c1">// handlers</span>
<span class="kd">const</span> <span class="nx">listProducts</span> <span class="o">=</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{};</span>
<span class="kd">const</span> <span class="nx">readProduct</span> <span class="o">=</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{};</span>
<span class="kd">const</span> <span class="nx">createProduct</span> <span class="o">=</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{};</span>
<span class="kd">const</span> <span class="nx">updateProduct</span> <span class="o">=</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{};</span>
<span class="kd">const</span> <span class="nx">deleteProduct</span> <span class="o">=</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{};</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">listProducts</span><span class="p">,</span>
<span class="nx">readProduct</span><span class="p">,</span>
<span class="nx">createProduct</span><span class="p">,</span>
<span class="nx">updateProduct</span><span class="p">,</span>
<span class="nx">deleteProduct</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Test it out:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm start
</code></pre></div></div>
<p>You should see:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Server listing on port 3000
</code></pre></div></div>
<p>Kill the server before moving on.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── client
│ ├── app.js
│ ├── package-lock.json
│ ├── package.json
│ └── routes
│ ├── grpcRoutes.js
│ └── productRoutes.js
├── protos
│ └── product.proto
└── server
├── db
│ ├── migrations
│ │ └── 20190131084532_products.js
│ └── seeds
│ └── products.js
├── index.js
├── knexfile.js
├── package-lock.json
└── package.json
</code></pre></div></div>
<p>With that, we’re now ready to tie everything together.</p>
<h2 id="crud">CRUD</h2>
<p>We’ll use the following workflow for wiring up the gRPC stubs:</p>
<ol>
<li>Add the appropriate knex query to the server (<em>server/index.js</em>)</li>
<li>Add the associated handler to the client (<em>client/routes/grpcRoutes.js</em>)</li>
<li>Test it via cURL</li>
</ol>
<h3 id="listproducts">listProducts</h3>
<p>Server:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">listProducts</span><span class="p">(</span><span class="nx">call</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*
Using 'grpc.load'? Send back an array: 'callback(null, { data });'
*/</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'products'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">callback</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="p">{</span> <span class="na">products</span><span class="p">:</span> <span class="nx">data</span> <span class="p">});</span> <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Client:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">listProducts</span> <span class="o">=</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="cm">/*
gRPC method for reference:
listProducts(Empty) returns (ProductList)
*/</span>
<span class="nx">client</span><span class="p">.</span><span class="nx">listProducts</span><span class="p">({},</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">result</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">result</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>To test, run the client in one terminal window and the server in another. Then, run the following command:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://127.0.0.1:3000/api/products
</code></pre></div></div>
<p>You should see the products:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"products"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pencil"</span><span class="p">,</span><span class="w">
</span><span class="s2">"price"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.99"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pen"</span><span class="p">,</span><span class="w">
</span><span class="s2">"price"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.99"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h3 id="readproduct">readProduct</h3>
<p>Server:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">readProduct</span><span class="p">(</span><span class="nx">call</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'products'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">where</span><span class="p">({</span> <span class="na">id</span><span class="p">:</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">call</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span> <span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">callback</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">callback</span><span class="p">(</span><span class="s1">'That product does not exist'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Client:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">readProduct</span> <span class="o">=</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span> <span class="p">};</span>
<span class="cm">/*
gRPC method for reference:
readProduct(ProductId) returns (Product)
*/</span>
<span class="nx">client</span><span class="p">.</span><span class="nx">readProduct</span><span class="p">(</span><span class="nx">payload</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">result</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="s1">'That product does not exist.'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">result</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Test success:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://127.0.0.1:3000/api/products/1
<span class="o">{</span>
<span class="s2">"id"</span>: 1,
<span class="s2">"name"</span>: <span class="s2">"pencil"</span>,
<span class="s2">"price"</span>: <span class="s2">"1.99"</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Test failure:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://127.0.0.1:3000/api/products/99999
<span class="s2">"That product does not exist."</span>
</code></pre></div></div>
<h3 id="createproduct">createProduct</h3>
<p>Server:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createProduct</span><span class="p">(</span><span class="nx">call</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'products'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">insert</span><span class="p">({</span>
<span class="na">name</span><span class="p">:</span> <span class="nx">call</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
<span class="na">price</span><span class="p">:</span> <span class="nx">call</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">price</span><span class="p">,</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">callback</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span> <span class="p">});</span> <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Client:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">createProduct</span> <span class="o">=</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="na">price</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">price</span> <span class="p">};</span>
<span class="cm">/*
gRPC method for reference:
createProduct(newProduct) returns (result)
*/</span>
<span class="nx">client</span><span class="p">.</span><span class="nx">createProduct</span><span class="p">(</span><span class="nx">payload</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">result</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">result</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Test:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-X</span> POST <span class="nt">-d</span> <span class="s1">'{"name":"lamp","price":"29.99"}'</span> <span class="se">\</span>
<span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> http://127.0.0.1:3000/api/products
<span class="o">{</span>
<span class="s2">"status"</span>: <span class="s2">"success"</span>
<span class="o">}</span>
<span class="nv">$ </span>curl http://127.0.0.1:3000/api/products
<span class="o">{</span>
<span class="s2">"products"</span>: <span class="o">[</span>
<span class="o">{</span>
<span class="s2">"id"</span>: 1,
<span class="s2">"name"</span>: <span class="s2">"pencil"</span>,
<span class="s2">"price"</span>: <span class="s2">"1.99"</span>
<span class="o">}</span>,
<span class="o">{</span>
<span class="s2">"id"</span>: 2,
<span class="s2">"name"</span>: <span class="s2">"pen"</span>,
<span class="s2">"price"</span>: <span class="s2">"2.99"</span>
<span class="o">}</span>,
<span class="o">{</span>
<span class="s2">"id"</span>: 3,
<span class="s2">"name"</span>: <span class="s2">"lamp"</span>,
<span class="s2">"price"</span>: <span class="s2">"29.99"</span>
<span class="o">}</span>
<span class="o">]</span>
<span class="o">}</span>
</code></pre></div></div>
<h3 id="updateproduct">updateProduct</h3>
<p>Server:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">updateProduct</span><span class="p">(</span><span class="nx">call</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'products'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">where</span><span class="p">({</span> <span class="na">id</span><span class="p">:</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">call</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span> <span class="p">})</span>
<span class="p">.</span><span class="nx">update</span><span class="p">({</span>
<span class="na">name</span><span class="p">:</span> <span class="nx">call</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
<span class="na">price</span><span class="p">:</span> <span class="nx">call</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">price</span><span class="p">,</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">returning</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">callback</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span> <span class="p">});</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">callback</span><span class="p">(</span><span class="s1">'That product does not exist'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Client:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">updateProduct</span> <span class="o">=</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">),</span> <span class="na">name</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="na">price</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">price</span> <span class="p">};</span>
<span class="cm">/*
gRPC method for reference:
updateProduct(Product) returns (result)
*/</span>
<span class="nx">client</span><span class="p">.</span><span class="nx">updateProduct</span><span class="p">(</span><span class="nx">payload</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">result</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="s1">'That product does not exist.'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">result</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Test:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-X</span> PUT <span class="nt">-d</span> <span class="s1">'{"name":"lamp","price":"49.99"}'</span> <span class="se">\</span>
<span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> http://127.0.0.1:3000/api/products/3
<span class="o">{</span>
<span class="s2">"status"</span>: <span class="s2">"success"</span>
<span class="o">}</span>
<span class="nv">$ </span>curl http://127.0.0.1:3000/api/products/3
<span class="o">{</span>
<span class="s2">"id"</span>: 3,
<span class="s2">"name"</span>: <span class="s2">"lamp"</span>,
<span class="s2">"price"</span>: <span class="s2">"49.99"</span>
<span class="o">}</span>
</code></pre></div></div>
<h3 id="deleteproduct">deleteProduct</h3>
<p>Server:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">deleteProduct</span><span class="p">(</span><span class="nx">call</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'products'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">where</span><span class="p">({</span> <span class="na">id</span><span class="p">:</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">call</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span> <span class="p">})</span>
<span class="p">.</span><span class="k">delete</span><span class="p">()</span>
<span class="p">.</span><span class="nx">returning</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">callback</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span> <span class="p">});</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">callback</span><span class="p">(</span><span class="s1">'That product does not exist'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Client:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">deleteProduct</span> <span class="o">=</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span> <span class="p">};</span>
<span class="cm">/*
gRPC method for reference:
deleteProduct(ProductId) returns (result)
*/</span>
<span class="nx">client</span><span class="p">.</span><span class="nx">deleteProduct</span><span class="p">(</span><span class="nx">payload</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">result</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="s1">'That product does not exist.'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">result</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Test:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-X</span> DELETE http://127.0.0.1:3000/api/products/3
<span class="o">{</span>
<span class="s2">"status"</span>: <span class="s2">"success"</span>
<span class="o">}</span>
<span class="nv">$ </span>curl http://127.0.0.1:3000/api/products
<span class="o">{</span>
<span class="s2">"products"</span>: <span class="o">[</span>
<span class="o">{</span>
<span class="s2">"id"</span>: 1,
<span class="s2">"name"</span>: <span class="s2">"pencil"</span>,
<span class="s2">"price"</span>: <span class="s2">"1.99"</span>
<span class="o">}</span>,
<span class="o">{</span>
<span class="s2">"id"</span>: 2,
<span class="s2">"name"</span>: <span class="s2">"pen"</span>,
<span class="s2">"price"</span>: <span class="s2">"2.99"</span>
<span class="o">}</span>
<span class="o">]</span>
<span class="o">}</span>
</code></pre></div></div>
<p>What if the product doesn’t exist?</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-X</span> DELETE http://127.0.0.1:3000/api/products/9999
<span class="s2">"That product does not exist."</span>
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>gRPC provides a declarative, strongly typed mechanism for defining an API.</p>
<p>Workflow:</p>
<ol>
<li>Define the API (service definition and message structure) using a Protocol Buffer</li>
<li>Implement the server using the generated interface</li>
<li>Add the message stubs to the client</li>
</ol>
<p>Resources:</p>
<ol>
<li><a href="https://github.com/mjhea0/node-grpc-crud">Final code</a></li>
<li><a href="https://grpc.io/docs/guides/index.html">What is gRPC?</a> guide</li>
<li><a href="https://blog.bugsnag.com/grpc-and-microservices-architecture/">Building scalable microservices with gRPC</a></li>
<li><a href="https://github.com/grpc/grpc-web">gRPC for Web Clients</a></li>
</ol>
Michael Herman
gRPC is an open source remote procedure call (RPC) framework developed by Google. It’s built on top of HTTP/2, and it uses Protocol Buffers as the underlying data serialization format.
Logging in Kubernetes with Elasticsearch, Kibana, and Fluentd
2019-01-20T00:00:00-06:00
2019-01-20T00:00:00-06:00
https://mherman.org/blog/logging-in-kubernetes-with-elasticsearch-Kibana-fluentd
<p>This tutorial looks at how to spin up a single node Elasticsearch cluster along with Kibana and Fluentd on Kubernetes.</p>
<p><em>Dependencies</em>:</p>
<ol>
<li>Docker v18.09.1</li>
<li>Kubernetes v1.13.2</li>
<li>Elasticsearch v6.5.4</li>
<li>Kibana v6.5.4</li>
<li>Fluentd v1.3.2</li>
</ol>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#minikube" id="markdown-toc-minikube">Minikube</a></li>
<li><a href="#elastic" id="markdown-toc-elastic">Elastic</a></li>
<li><a href="#kibana" id="markdown-toc-kibana">Kibana</a></li>
<li><a href="#fluentd" id="markdown-toc-fluentd">Fluentd</a></li>
<li><a href="#sanity-check" id="markdown-toc-sanity-check">Sanity Check</a></li>
</ul>
<h2 id="minikube">Minikube</h2>
<p><a href="https://kubernetes.io/docs/setup/minikube/">Minikube</a> is a tool that makes it easy for developers to use and run a “toy” Kubernetes cluster locally. It’s a great way to quickly get a cluster up and running so you can start interacting with the Kubernetes API.</p>
<blockquote>
<p>If you already have a Kubernetes cluster up and running that you’d like to use, you can skip this section.</p>
</blockquote>
<p>Follow the official <a href="https://kubernetes.io/docs/setup/minikube/#quickstart">quickstart guide</a> to get Minikube installed along with:</p>
<ol>
<li>A <a href="https://kubernetes.io/docs/tasks/tools/install-minikube/#install-a-hypervisor">Hypervisor</a> (like <a href="https://www.virtualbox.org/wiki/Downloads">VirtualBox</a> or <a href="https://github.com/moby/hyperkit">HyperKit</a>) to manage virtual machines</li>
<li><a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">Kubectl</a> to deploy and manage apps on Kubernetes</li>
</ol>
<p>If you’re on a Mac, we recommend installing Kubectl, Hyperkit, and Minikube with <a href="https://brew.sh/">Homebrew</a>:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>brew update
<span class="nv">$ </span>brew install kubectl
<span class="nv">$ </span>brew install docker-machine-driver-hyperkit
<span class="nv">$ </span>brew cask install minikube
</code></pre></div></div>
<p>By default, the Minikube VM is configured to use 1GB of memory and 2 CPU cores. This is not sufficient for Elasticsearch, so be sure to increase the memory in your Docker <a href="https://docs.docker.com/docker-for-mac/#advanced">client</a> (for HyperKit) or directly in VirtualBox. Then, when you start Minikube, pass the memory and CPU options to it:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>minikube start <span class="nt">--vm-driver</span><span class="o">=</span>hyperkit <span class="nt">--memory</span> 8192 <span class="nt">--cpus</span> 4
or
<span class="nv">$ </span>minikube start <span class="nt">--memory</span> 8192 <span class="nt">--cpus</span> 4
</code></pre></div></div>
<p>Once up, make sure you can view the dashboard:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>minikube dashboard
</code></pre></div></div>
<p>Then, create a new namespace:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl create namespace logging
namespace/logging created
</code></pre></div></div>
<p>If you run into problems with Minikube, it’s often best to remove it completely and start over.</p>
<p>For example:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>minikube stop<span class="p">;</span> minikube delete
<span class="nv">$ </span>rm /usr/local/bin/minikube
<span class="nv">$ </span>rm <span class="nt">-rf</span> ~/.minikube
<span class="c"># re-download minikube</span>
<span class="nv">$ </span>minikube start
</code></pre></div></div>
<h2 id="elastic">Elastic</h2>
<p>We’ll start with <a href="https://www.elastic.co/products/elasticsearch">Elasticsearch</a>.</p>
<blockquote>
<p>You can find the code in the <a href="https://github.com/mjhea0/efk-kubernetes">efk-kubernetes</a> repo on GitHub.</p>
</blockquote>
<p><em>kubernetes/elastic.yaml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">extensions/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">elasticsearch</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">selector</span><span class="pi">:</span>
<span class="na">matchLabels</span><span class="pi">:</span>
<span class="na">component</span><span class="pi">:</span> <span class="s">elasticsearch</span>
<span class="na">template</span><span class="pi">:</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">component</span><span class="pi">:</span> <span class="s">elasticsearch</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">elasticsearch</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">docker.elastic.co/elasticsearch/elasticsearch:6.5.4</span>
<span class="na">env</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">discovery.type</span>
<span class="na">value</span><span class="pi">:</span> <span class="s">single-node</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="s">9200</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
<span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
<span class="na">resources</span><span class="pi">:</span>
<span class="na">limits</span><span class="pi">:</span>
<span class="na">cpu</span><span class="pi">:</span> <span class="s">500m</span>
<span class="na">memory</span><span class="pi">:</span> <span class="s">4Gi</span>
<span class="na">requests</span><span class="pi">:</span>
<span class="na">cpu</span><span class="pi">:</span> <span class="s">500m</span>
<span class="na">memory</span><span class="pi">:</span> <span class="s">4Gi</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">elasticsearch</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">service</span><span class="pi">:</span> <span class="s">elasticsearch</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">NodePort</span>
<span class="na">selector</span><span class="pi">:</span>
<span class="na">component</span><span class="pi">:</span> <span class="s">elasticsearch</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="s">9200</span>
<span class="na">targetPort</span><span class="pi">:</span> <span class="s">9200</span>
</code></pre></div></div>
<p>So, this will spin up a <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html#single-node-discovery">single node</a> Elasticsearch pod in the cluster along with a <a href="https://kubernetes.io/docs/concepts/services-networking/service/#nodeport">NodePort</a> service to expose the pod to the outside world.</p>
<p>Create:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl create <span class="nt">-f</span> kubernetes/elastic.yaml <span class="nt">-n</span> logging
deployment.extensions/elasticsearch created
service/elasticsearch created
</code></pre></div></div>
<p>Verify that both the pod and service were created:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get pods <span class="nt">-n</span> logging
NAME READY STATUS RESTARTS AGE
elasticsearch-bb9f879-d9kmg 1/1 Running 0 75s
<span class="nv">$ </span>kubectl get service <span class="nt">-n</span> logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT<span class="o">(</span>S<span class="o">)</span> AGE
elasticsearch NodePort 10.102.149.212 <none> 9200:30531/TCP 86s
</code></pre></div></div>
<p>Take note of the exposed port–e.g, <code class="highlighter-rouge">30531</code>. Then, grab the Minikube IP and make sure you can cURL that pod:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="k">$(</span>minikube ip<span class="k">)</span>:30531
</code></pre></div></div>
<p>You should see something similar to:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">{</span>
<span class="s2">"name"</span> : <span class="s2">"9b5Whvv"</span>,
<span class="s2">"cluster_name"</span> : <span class="s2">"docker-cluster"</span>,
<span class="s2">"cluster_uuid"</span> : <span class="s2">"-qMwo61ATv2KmbZsf2_Tpw"</span>,
<span class="s2">"version"</span> : <span class="o">{</span>
<span class="s2">"number"</span> : <span class="s2">"6.5.4"</span>,
<span class="s2">"build_flavor"</span> : <span class="s2">"default"</span>,
<span class="s2">"build_type"</span> : <span class="s2">"tar"</span>,
<span class="s2">"build_hash"</span> : <span class="s2">"d2ef93d"</span>,
<span class="s2">"build_date"</span> : <span class="s2">"2018-12-17T21:17:40.758843Z"</span>,
<span class="s2">"build_snapshot"</span> : <span class="nb">false</span>,
<span class="s2">"lucene_version"</span> : <span class="s2">"7.5.0"</span>,
<span class="s2">"minimum_wire_compatibility_version"</span> : <span class="s2">"5.6.0"</span>,
<span class="s2">"minimum_index_compatibility_version"</span> : <span class="s2">"5.0.0"</span>
<span class="o">}</span>,
<span class="s2">"tagline"</span> : <span class="s2">"You Know, for Search"</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="kibana">Kibana</h2>
<p>Next, let’s get <a href="https://www.elastic.co/products/kibana">Kibana</a> up and running.</p>
<p><em>kubernetes/kibana.yaml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">extensions/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">kibana</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">selector</span><span class="pi">:</span>
<span class="na">matchLabels</span><span class="pi">:</span>
<span class="na">run</span><span class="pi">:</span> <span class="s">kibana</span>
<span class="na">template</span><span class="pi">:</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">run</span><span class="pi">:</span> <span class="s">kibana</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">kibana</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">docker.elastic.co/kibana/kibana:6.5.4</span>
<span class="na">env</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ELASTICSEARCH_URL</span>
<span class="na">value</span><span class="pi">:</span> <span class="s">http://elasticsearch:9200</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">XPACK_SECURITY_ENABLED</span>
<span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="s">5601</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
<span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">kibana</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">service</span><span class="pi">:</span> <span class="s">kibana</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">NodePort</span>
<span class="na">selector</span><span class="pi">:</span>
<span class="na">run</span><span class="pi">:</span> <span class="s">kibana</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="s">5601</span>
<span class="na">targetPort</span><span class="pi">:</span> <span class="s">5601</span>
</code></pre></div></div>
<p>Like before, this deployment will spin up a single Kibana pod that gets exposed via a NodePort service. Take note of the two environment variables:</p>
<ol>
<li><code class="highlighter-rouge">ELASTICSEARCH_URL</code> - URL of the Elasticsearch instance</li>
<li><code class="highlighter-rouge">XPACK_SECURITY_ENABLED</code> - enables <a href="https://www.elastic.co/guide/en/x-pack/current/elasticsearch-security.html">X-Pack security</a></li>
</ol>
<p>Refer to the <a href="https://www.elastic.co/guide/en/kibana/current/docker.html">Running Kibana on Docker</a> guide for more info on these variables.</p>
<p>Create:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl create <span class="nt">-f</span> kubernetes/kibana.yaml <span class="nt">-n</span> logging
</code></pre></div></div>
<p>Verify:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get pods <span class="nt">-n</span> logging
NAME READY STATUS RESTARTS AGE
elasticsearch-bb9f879-d9kmg 1/1 Running 0 17m
kibana-7f6686674c-mjlb2 1/1 Running 0 60s
<span class="nv">$ </span>kubectl get service <span class="nt">-n</span> logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT<span class="o">(</span>S<span class="o">)</span> AGE
elasticsearch NodePort 10.102.149.212 <none> 9200:30531/TCP 17m
kibana NodePort 10.106.226.34 <none> 5601:32683/TCP 74s
</code></pre></div></div>
<p>Test this in your browser at <code class="highlighter-rouge">http://MINIKUBE_IP:KIBANA_EXPOSED_PORT</code>.</p>
<p><img src="/assets/img/blog/kibana/kibana.png" alt="kibana" /></p>
<h2 id="fluentd">Fluentd</h2>
<p>In this example, we’ll deploy a Fluentd logging agent to each node in the Kubernetes cluster, which will collect each container’s log files running on that node. We can use a <a href="https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/">DaemonSet</a> for this.</p>
<p><img src="/assets/img/blog/kibana/fluentd-kubernetes.png" alt="fluentd and kubernetes" /></p>
<p>First, we need to configure RBAC (role-based access control) permissions so that Fluentd can access the appropriate components.</p>
<p><em>kubernetes/fluentd-rbac.yaml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ServiceAccount</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">fluentd</span>
<span class="na">namespace</span><span class="pi">:</span> <span class="s">kube-system</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">rbac.authorization.k8s.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterRole</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">fluentd</span>
<span class="na">namespace</span><span class="pi">:</span> <span class="s">kube-system</span>
<span class="na">rules</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">apiGroups</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">"</span>
<span class="na">resources</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">pods</span>
<span class="pi">-</span> <span class="s">namespaces</span>
<span class="na">verbs</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">get</span>
<span class="pi">-</span> <span class="s">list</span>
<span class="pi">-</span> <span class="s">watch</span>
<span class="nn">---</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterRoleBinding</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">rbac.authorization.k8s.io/v1beta1</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">fluentd</span>
<span class="na">roleRef</span><span class="pi">:</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterRole</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">fluentd</span>
<span class="na">apiGroup</span><span class="pi">:</span> <span class="s">rbac.authorization.k8s.io</span>
<span class="na">subjects</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">kind</span><span class="pi">:</span> <span class="s">ServiceAccount</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">fluentd</span>
<span class="na">namespace</span><span class="pi">:</span> <span class="s">kube-system</span>
</code></pre></div></div>
<p>In short, this will create a ClusterRole which grants get, list, and watch permissions on pods and namespace objects. The ClusterRoleBinding then binds the ClusterRole to the ServiceAccount within the <code class="highlighter-rouge">kube-system</code> namespace. Refer to the <a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">Using RBAC Authorization</a> guide to learn more about RBAC and ClusterRoles.</p>
<p>Create:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl create <span class="nt">-f</span> kubernetes/fluentd-rbac.yaml
</code></pre></div></div>
<p>Now, we can create the DaemonSet.</p>
<p><em>kubernetes/fluentd-daemonset.yaml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">extensions/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">DaemonSet</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">fluentd</span>
<span class="na">namespace</span><span class="pi">:</span> <span class="s">kube-system</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">k8s-app</span><span class="pi">:</span> <span class="s">fluentd-logging</span>
<span class="na">version</span><span class="pi">:</span> <span class="s">v1</span>
<span class="s">kubernetes.io/cluster-service</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">template</span><span class="pi">:</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">k8s-app</span><span class="pi">:</span> <span class="s">fluentd-logging</span>
<span class="na">version</span><span class="pi">:</span> <span class="s">v1</span>
<span class="s">kubernetes.io/cluster-service</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">serviceAccount</span><span class="pi">:</span> <span class="s">fluentd</span>
<span class="na">serviceAccountName</span><span class="pi">:</span> <span class="s">fluentd</span>
<span class="na">tolerations</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s">node-role.kubernetes.io/master</span>
<span class="na">effect</span><span class="pi">:</span> <span class="s">NoSchedule</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">fluentd</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">fluent/fluentd-kubernetes-daemonset:v1.3-debian-elasticsearch</span>
<span class="na">env</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">FLUENT_ELASTICSEARCH_HOST</span>
<span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">elasticsearch.logging"</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">FLUENT_ELASTICSEARCH_PORT</span>
<span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">9200"</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">FLUENT_ELASTICSEARCH_SCHEME</span>
<span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http"</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">FLUENT_UID</span>
<span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0"</span>
<span class="na">resources</span><span class="pi">:</span>
<span class="na">limits</span><span class="pi">:</span>
<span class="na">memory</span><span class="pi">:</span> <span class="s">200Mi</span>
<span class="na">requests</span><span class="pi">:</span>
<span class="na">cpu</span><span class="pi">:</span> <span class="s">100m</span>
<span class="na">memory</span><span class="pi">:</span> <span class="s">200Mi</span>
<span class="na">volumeMounts</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">varlog</span>
<span class="na">mountPath</span><span class="pi">:</span> <span class="s">/var/log</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">varlibdockercontainers</span>
<span class="na">mountPath</span><span class="pi">:</span> <span class="s">/var/lib/docker/containers</span>
<span class="na">readOnly</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">terminationGracePeriodSeconds</span><span class="pi">:</span> <span class="s">30</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">varlog</span>
<span class="na">hostPath</span><span class="pi">:</span>
<span class="na">path</span><span class="pi">:</span> <span class="s">/var/log</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">varlibdockercontainers</span>
<span class="na">hostPath</span><span class="pi">:</span>
<span class="na">path</span><span class="pi">:</span> <span class="s">/var/lib/docker/containers</span>
</code></pre></div></div>
<p>Be sure to review <a href="https://docs.fluentd.org/v/0.12/articles/kubernetes-fluentd">Kubernetes Logging with Fluentd</a> along with the <a href="https://github.com/fluent/fluentd-kubernetes-daemonset/blob/master/fluentd-daemonset-elasticsearch-rbac.yaml">sample Daemonset</a>. Make sure <code class="highlighter-rouge">FLUENT_ELASTICSEARCH_HOST</code> aligns with the <code class="highlighter-rouge">SERVICE_NAME.NAMESPACE</code> of Elasticsearch within your cluster.</p>
<p>Deploy:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl create <span class="nt">-f</span> kubernetes/fluentd-daemonset.yaml
</code></pre></div></div>
<p>If you’re running Kubernetes as a single node with Minikube, this will create a single Fluentd pod in the <code class="highlighter-rouge">kube-system</code> namespace.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get pods <span class="nt">-n</span> kube-system
coredns-576cbf47c7-mhxbp 1/1 Running 0 120m
coredns-576cbf47c7-vx7m7 1/1 Running 0 120m
etcd-minikube 1/1 Running 0 119m
fluentd-kxc46 1/1 Running 0 89s
kube-addon-manager-minikube 1/1 Running 0 119m
kube-apiserver-minikube 1/1 Running 0 119m
kube-controller-manager-minikube 1/1 Running 0 119m
kube-proxy-m4vzt 1/1 Running 0 120m
kube-scheduler-minikube 1/1 Running 0 119m
kubernetes-dashboard-5bff5f8fb8-d64qs 1/1 Running 0 120m
storage-provisioner 1/1 Running 0 120m
</code></pre></div></div>
<p>Take note of the logs:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl logs fluentd-kxc46 <span class="nt">-n</span> kube-system
</code></pre></div></div>
<p>You should see that Fluentd connect to Elasticsearch within the logs:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Connection opened to Elasticsearch cluster <span class="o">=></span>
<span class="o">{</span>:host<span class="o">=></span><span class="s2">"elasticsearch.logging"</span>, :port<span class="o">=></span>9200, :scheme<span class="o">=></span><span class="s2">"http"</span><span class="o">}</span>
</code></pre></div></div>
<p>To see the logs collected by Fluentd in Kibana, click “Management” and then select “Index Patterns” under “Kibana”.</p>
<p><img src="/assets/img/blog/kibana/kibana2.png" alt="kibana" /></p>
<p>Click the “Create index pattern” button. Select the new Logstash index that is generated by the Fluentd DaemonSet. Click “Next step”.</p>
<p><img src="/assets/img/blog/kibana/kibana3.png" alt="kibana" /></p>
<p>Set the “Time Filter field name” to “@timestamp”. Then, click “Create index pattern”.</p>
<p><img src="/assets/img/blog/kibana/kibana4.png" alt="kibana" /></p>
<p>Click “Discover” to view your application logs.</p>
<h2 id="sanity-check">Sanity Check</h2>
<p>Let’s spin up a quick Node.js app to test.</p>
<p>Point your local Docker client at the Minikube Docker daemon, and then build the image:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">eval</span> <span class="k">$(</span>minikube docker-env<span class="k">)</span>
<span class="nv">$ </span>docker build <span class="nt">-t</span> fluentd-node-sample:latest <span class="nt">-f</span> sample-app/Dockerfile sample-app
</code></pre></div></div>
<p>Create the deployment:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl create <span class="nt">-f</span> kubernetes/node-deployment.yaml
</code></pre></div></div>
<p>Take a quick look at the app in <em>sample-app/index.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">SimpleNodeLogger</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'simple-node-logger'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">opts</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">timestampFormat</span><span class="p">:</span><span class="s1">'YYYY-MM-DD HH:mm:ss.SSS'</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">log</span> <span class="o">=</span> <span class="nx">SimpleNodeLogger</span><span class="p">.</span><span class="nx">createSimpleLogger</span><span class="p">(</span><span class="nx">opts</span><span class="p">);</span>
<span class="p">(</span><span class="kd">function</span> <span class="nx">repeatMe</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">log</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="s1">'it works'</span><span class="p">);</span>
<span class="nx">repeatMe</span><span class="p">();</span>
<span class="p">},</span> <span class="mi">1000</span><span class="p">)</span>
<span class="p">})();</span>
</code></pre></div></div>
<p>So, it just logs <code class="highlighter-rouge">'it works'</code> to stdout every second.</p>
<p>Back in “Discover” on the Kibana dashboard, add the following filter:</p>
<ol>
<li>Field: <code class="highlighter-rouge">kubernetes.pod_name</code></li>
<li>Operator: <code class="highlighter-rouge">is</code></li>
<li>Value: <code class="highlighter-rouge">node</code></li>
</ol>
<p><img src="/assets/img/blog/kibana/kibana5.png" alt="kibana" /></p>
<p>Now, you should be able to see the <code class="highlighter-rouge">it works</code> log in the stream.</p>
<p><img src="/assets/img/blog/kibana/kibana6.png" alt="kibana" /></p>
<hr />
<p>Again, you can find the code in the <a href="https://github.com/mjhea0/efk-kubernetes">efk-kubernetes</a> repo on GitHub.</p>
Michael Herman
This tutorial looks at how to spin up a single node Elasticsearch cluster along with Kibana and Fluentd on Kubernetes.
Setting Up a Kubernetes Cluster on Ubuntu 18.04
2018-08-20T00:00:00-05:00
2018-08-20T00:00:00-05:00
https://mherman.org/blog/setting-up-a-kubernetes-cluster-on-ubuntu
<p>In this tutorial, we’ll spin up a Kubernetes cluster on Ubuntu 18.04.</p>
<blockquote>
<p>This guide uses Ubuntu 18.04 droplets on <a href="https://m.do.co/c/d8f211a4b4c2">DigitalOcean</a>. Feel free to swap out DigitalOcean for a different cloud hosting provider or your own on-premise environment.</p>
</blockquote>
<p><em>Dependencies</em>:</p>
<ol>
<li>Docker v18.06.0-ce</li>
<li>Kubernetes v1.11.2</li>
</ol>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#provision-the-machines" id="markdown-toc-provision-the-machines">Provision the Machines</a></li>
<li><a href="#configure-the-master-node" id="markdown-toc-configure-the-master-node">Configure the Master Node</a></li>
<li><a href="#configure-the-worker-nodes" id="markdown-toc-configure-the-worker-nodes">Configure the Worker Nodes</a></li>
<li><a href="#sanity-check" id="markdown-toc-sanity-check">Sanity Check</a></li>
</ul>
<h2 id="provision-the-machines">Provision the Machines</h2>
<p>Following along with the DigitalOcean example?</p>
<ol>
<li><a href="https://m.do.co/c/d8f211a4b4c2">Sign up</a> for an account (if you don’t already have one).</li>
<li><a href="https://www.digitalocean.com/community/tutorials/how-to-use-the-digitalocean-api-v2">Generate</a> an access token so you can access the DigitalOcean API.</li>
<li>Spin up three new <a href="https://www.digitalocean.com/pricing/">droplets</a> (one master and two workers) that have 4 GB of memory and 2 vCPUs either from the DigitalOcean console or <a href="https://github.com/digitalocean/doctl">doctl</a>, the DigitalOcean.</li>
</ol>
<p><code class="highlighter-rouge">doctl</code> example:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># authenticate</span>
<span class="nv">$ </span>doctl auth init
<span class="c"># spin up the droplets</span>
<span class="nv">$ </span><span class="k">for </span>i <span class="k">in </span>1 2 3<span class="p">;</span> <span class="k">do
</span>doctl compute droplet create <span class="se">\</span>
node-<span class="nv">$i</span> <span class="se">\</span>
<span class="nt">--region</span> nyc1 <span class="se">\</span>
<span class="nt">--image</span> ubuntu-18-04-x64 <span class="se">\</span>
<span class="nt">--size</span> 4gb
<span class="k">done</span>
</code></pre></div></div>
<p>Wait a few seconds for the droplets to spin up, and then run:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>doctl compute droplet list
</code></pre></div></div>
<p>You should see something like:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ID Name Public IPv4 Memory VCPUs Disk Region Image Status
106096338 node-1 142.93.198.67 4096 2 60 nyc1 Ubuntu 18.04 x64 active
106096340 node-2 142.93.198.167 4096 2 60 nyc1 Ubuntu 18.04 x64 active
106096345 node-3 142.93.206.39 4096 2 60 nyc1 Ubuntu 18.04 x64 active
</code></pre></div></div>
<p>Take note of the public IP addresses.</p>
<blockquote>
<p>Refer to the <a href="https://www.digitalocean.com/community/tutorials/how-to-use-doctl-the-official-digitalocean-command-line-client">How To Use Doctl</a> guide for more info on using the <code class="highlighter-rouge">doctl</code> CLI tool.</p>
</blockquote>
<h2 id="configure-the-master-node">Configure the Master Node</h2>
<p>SSH into the machine that will serve as the Kubernetes <a href="https://kubernetes.io/docs/concepts/overview/components/#master-components">master</a>, and then run the following commands…</p>
<h3 id="install-dependencies">Install Dependencies</h3>
<p>Install Docker along with-</p>
<ol>
<li><a href="https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/">kubeadm</a> - bootstraps a Kubernetes cluster</li>
<li><a href="https://kubernetes.io/docs/reference/generated/kubelet/">kubelet</a> - configures containers to run on a host</li>
<li><a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">kubectl</a> - deploys and manages apps on Kubernetes</li>
</ol>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>apt-get update <span class="o">&&</span> apt-get install <span class="nt">-y</span> apt-transport-https
<span class="nv">$ </span>curl <span class="nt">-s</span> https://download.docker.com/linux/ubuntu/gpg | <span class="nb">sudo </span>apt-key add -
<span class="nv">$ </span>add-apt-repository <span class="se">\</span>
<span class="s2">"deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"</span>
<span class="nv">$ </span>apt update <span class="o">&&</span> apt install <span class="nt">-qy</span> docker-ce
<span class="nv">$ </span>curl <span class="nt">-s</span> https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"deb http://apt.kubernetes.io/ kubernetes-xenial main"</span> <span class="se">\</span>
<span class="o">></span> /etc/apt/sources.list.d/kubernetes.list
<span class="nv">$ </span>apt-get update <span class="o">&&</span> apt-get install <span class="nt">-y</span> kubeadm kubelet kubectl
</code></pre></div></div>
<blockquote>
<p>As of writing, Kubernetes for Ubuntu 18.04 (Bionic) is not yet available, but Ubuntu 16.04 (Xenial) works just fine. Also, Kubernetes 1.11 does <em>not</em> officially <a href="https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.11.md#external-dependencies">support</a> Docker 18.06. It should work fine, but it could bring up some unexpected issues.</p>
</blockquote>
<h3 id="configure-kubernetes">Configure Kubernetes</h3>
<p>Init Kubernetes:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubeadm init <span class="nt">--apiserver-advertise-address</span><span class="o">=</span><MASTER_IP> <span class="nt">--pod-network-cidr</span><span class="o">=</span>192.168.1.0/16
</code></pre></div></div>
<blockquote>
<p>Be sure to replace <code class="highlighter-rouge"><MASTER_IP></code> with the machine’s IP address.</p>
</blockquote>
<p>Take note of the join token command. For example:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeadm join 142.93.198.67:6443 <span class="se">\</span>
<span class="nt">--token</span> 58f5m6.rms4ycepgmtvjl9z <span class="nt">--discovery-token-ca-cert-hash</span> sha256:<<span class="nb">hash</span><span class="o">></span>
</code></pre></div></div>
<p>Then, create a new user:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>adduser michael
<span class="nv">$ </span>usermod <span class="nt">-aG</span> <span class="nb">sudo </span>michael
<span class="nv">$ </span>su - michael
</code></pre></div></div>
<p>Set up the Kubernetes config as the user you created:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>mkdir <span class="nt">-p</span> <span class="nv">$HOME</span>/.kube
<span class="nv">$ </span><span class="nb">sudo </span>cp <span class="nt">-i</span> /etc/kubernetes/admin.conf <span class="nv">$HOME</span>/.kube/config
<span class="nv">$ </span><span class="nb">sudo </span>chown <span class="k">$(</span>id <span class="nt">-u</span><span class="k">)</span>:<span class="k">$(</span>id <span class="nt">-g</span><span class="k">)</span> <span class="nv">$HOME</span>/.kube/config
</code></pre></div></div>
<p>Again, as the new user, deploy a <a href="https://github.com/coreos/flannel">flannel</a> network to the cluster:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl apply <span class="nt">-f</span> https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
</code></pre></div></div>
<h2 id="configure-the-worker-nodes">Configure the Worker Nodes</h2>
<p>The following commands should be run on each of the worker machines.</p>
<h3 id="install-dependencies-1">Install Dependencies</h3>
<p>Again, install Docker, kubeadm, kubelet, and kubectl:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>apt-get update <span class="o">&&</span> apt-get install <span class="nt">-y</span> apt-transport-https
<span class="nv">$ </span>curl <span class="nt">-s</span> https://download.docker.com/linux/ubuntu/gpg | <span class="nb">sudo </span>apt-key add -
<span class="nv">$ </span>add-apt-repository <span class="se">\</span>
<span class="s2">"deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"</span>
<span class="nv">$ </span>apt update <span class="o">&&</span> apt install <span class="nt">-qy</span> docker-ce
<span class="nv">$ </span>curl <span class="nt">-s</span> https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
<span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"deb http://apt.kubernetes.io/ kubernetes-xenial main"</span> <span class="se">\</span>
<span class="o">></span> /etc/apt/sources.list.d/kubernetes.list
<span class="nv">$ </span>apt-get update <span class="o">&&</span> apt-get install <span class="nt">-y</span> kubeadm kubelet kubectl
</code></pre></div></div>
<h3 id="configure-kubernetes-1">Configure Kubernetes</h3>
<p>Using the join command from above, join the worker to the cluster.</p>
<h2 id="sanity-check">Sanity Check</h2>
<p>Back on the master node, run the following command as the user you created:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get nodes
</code></pre></div></div>
<p>You should see something similar to:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NAME STATUS ROLES AGE VERSION
node-1 Ready master 28m v1.11.2
node-2 Ready <none> 3m v1.11.2
node-3 Ready <none> 35s v1.11.2
</code></pre></div></div>
<p>Remove the droplets once done:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="k">for </span>i <span class="k">in </span>1 2 3<span class="p">;</span> <span class="k">do
</span>doctl compute droplet delete node-<span class="nv">$i</span>
<span class="k">done</span>
</code></pre></div></div>
<hr />
<p>That’s it!</p>
<blockquote>
<p>Want to automate this process? Check out <a href="https://testdriven.io/creating-a-kubernetes-cluster-on-digitalocean">Creating a Kubernetes Cluster on DigitalOcean with Python and Fabric</a>.</p>
</blockquote>
Michael Herman
In this tutorial, we’ll spin up a Kubernetes cluster on Ubuntu 18.04.
Authentication in Angular with NGRX
2018-04-17T00:00:00-05:00
2018-04-17T00:00:00-05:00
https://mherman.org/blog/authentication-in-angular-with-ngrx
<p>In this tutorial, we’ll add authentication to Angular using <a href="https://github.com/ngrx/platform">NGRX</a> Store and Effects.</p>
<blockquote>
<p>This post assumes that you a have basic working knowledge of <a href="https://angular.io/">Angular</a> (2+), <a href="https://www.typescriptlang.org/">TypeScript</a>, <a href="http://reactivex.io/rxjs/">RxJS</a> and the <a href="https://redux.js.org/">Redux</a> design pattern. If you’re new to <a href="http://ngrx.github.io/">NGRX</a>, check out the awesome <a href="https://gist.github.com/btroncone/a6e4347326749f938510">Comprehensive Introduction to @ngrx/store</a> guide.</p>
</blockquote>
<p><em>Dependencies</em>:</p>
<ol>
<li>Angular CLI v1.7.3 (Angular v5.2.0)</li>
<li>Node v9.11.0</li>
<li>NGRX Store v5.2.0</li>
<li>NGRX Effects v5.2.0</li>
</ol>
<p><em>Final app</em>:</p>
<div style="text-align:left;padding-top:10px;padding-left:10px;padding-bottom:10px">
<img src="/assets/img/blog/angular-auth-ngrx/final-app.gif" style="max-width: 70%;border:0;box-shadow:none;" alt="final app" />
</div>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#token-based-authentication" id="markdown-toc-token-based-authentication">Token-based Authentication</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#routing-and-components" id="markdown-toc-routing-and-components">Routing and Components</a></li>
<li><a href="#workflow" id="markdown-toc-workflow">Workflow</a></li>
<li><a href="#configure-forms-1" id="markdown-toc-configure-forms-1">Configure Forms</a></li>
<li><a href="#ngrx-store-1" id="markdown-toc-ngrx-store-1">NGRX Store</a></li>
<li><a href="#configure-auth-service-1" id="markdown-toc-configure-auth-service-1">Configure Auth Service</a></li>
<li><a href="#ngrx-effects-1" id="markdown-toc-ngrx-effects-1">NGRX Effects</a></li>
<li><a href="#configure-login-1" id="markdown-toc-configure-login-1">Configure Login</a></li>
<li><a href="#configure-signup-1" id="markdown-toc-configure-signup-1">Configure Signup</a></li>
<li><a href="#configure-logout-1" id="markdown-toc-configure-logout-1">Configure Logout</a></li>
<li><a href="#update-the-templates-1" id="markdown-toc-update-the-templates-1">Update the Templates</a></li>
<li><a href="#add-http-interceptor-1" id="markdown-toc-add-http-interceptor-1">Add HTTP Interceptor</a></li>
<li><a href="#route-guard-1" id="markdown-toc-route-guard-1">Route Guard</a></li>
<li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you will be able to…</p>
<ol>
<li>Develop a fully-functioning Angular app with authentication</li>
<li>Discuss the benefits and drawbacks of using token-based authentication</li>
<li>Implement user authentication with tokens</li>
<li>Utilize NGRX Store for state management</li>
<li>Isolate and manage side effects with NGRX Effects</li>
<li>Set up an HTTP Interceptor to hijack outgoing requests and incoming responses</li>
<li>Configure route-based authorization with route guards</li>
</ol>
<h2 id="token-based-authentication">Token-based Authentication</h2>
<p>With token-based Authentication, users send their credentials to an authentication server to obtain a signed token. The client then stores this token locally, usually in localStorage or in a cookie. Every subsequent call to the server, for a protected resource, includes that signed token that the server then verifies before granting access to the desired resource.</p>
<p><strong>Benefits:</strong></p>
<ol>
<li><em>Stateless</em>: Since the token contains all information required for the server to verify a user’s identity, scaling is easier as we don’t need to worry about maintaining a session store on the server-side.</li>
<li><em>Single Sign On</em>: After a token is generated, you can have your users access a number of different resources and services without having to prompt them for their login credentials.</li>
</ol>
<p><strong>Drawbacks:</strong></p>
<ol>
<li><em>Cross-site Scripting (XSS) attacks</em>: Storing tokens in either local or session storage can lead to XSS attacks. Because of this, it’s a good idea to store tokens in a cookie with <code class="highlighter-rouge">httpOnly</code> and <code class="highlighter-rouge">secure</code> flags.</li>
</ol>
<blockquote>
<p>Although we won’t be covering server-side token creation in this post, it’s worth noting that a <a href="https://jwt.io/">JSON Web Token</a> is a popular standard for creating tokens. Just keep in mind that since a JWT is <a href="https://stackoverflow.com/questions/454048/what-is-the-difference-between-encrypting-and-signing-in-asymmetric-encryption">signed rather than encrypted</a> it should never contain sensitive information like a user’s password.</p>
</blockquote>
<p>With that, here’s the full user auth process:</p>
<ol>
<li>End user sends their credentials to the server</li>
<li>Server verifies the credentials and, if correct, generates a token, which is then passed back to the client</li>
<li>Client stores the token in localStorage or in a cookie</li>
<li>Client sends the token alongside any subsequent requests to the server</li>
</ol>
<p>For more on token-based auth, along with the pros and cons of using it vs. session-based auth, please review the following articles:</p>
<ol>
<li><a href="https://dzone.com/articles/cookies-vs-tokens-the-definitive-guide">Cookies vs Tokens: The Definitive Guide</a></li>
<li><a href="https://stackoverflow.com/questions/17000835/token-authentication-vs-cookies">Token Authentication vs. Cookies</a></li>
</ol>
<h2 id="project-setup">Project Setup</h2>
<p>Begin by installing the Angular CLI globally, if you don’t already have it:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install <span class="nt">-g</span> @angular/cli@1.7.3
</code></pre></div></div>
<p>Then, create a new Angular project:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ng new angular-auth-ngrx
<span class="nv">$ </span><span class="nb">cd </span>angular-auth-ngrx
</code></pre></div></div>
<p>Run the server:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ng serve
</code></pre></div></div>
<p>Then, in your browser of choice, navigate to <a href="http://localhost:4200">http://localhost:4200</a>. You should see the “Welcome to app!” message along with the Angular logo in the browser.</p>
<p>Kill the server. Then, install <a href="http://getbootstrap.com/">Bootstrap</a>:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install bootstrap@4.1.0 <span class="nt">--save</span>
</code></pre></div></div>
<p>After installation, update the styles configuration in the <em>.angular-cli.json</em> file at the project root:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"styles"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"../node_modules/bootstrap/dist/css/bootstrap.min.css"</span><span class="p">,</span><span class="w">
</span><span class="s2">"styles.css"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span></code></pre></div></div>
<p>Add a <a href="https://getbootstrap.com/docs/4.0/layout/overview/#containers">container</a> to the <em>src/index.html</em> page:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><!doctype html></span>
<span class="nt"><html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">></span>
<span class="nt"><head></span>
<span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"utf-8"</span><span class="nt">></span>
<span class="nt"><title></span>AngularAuthNgrx<span class="nt"></title></span>
<span class="nt"><base</span> <span class="na">href=</span><span class="s">"/"</span><span class="nt">></span>
<span class="nt"><meta</span> <span class="na">name=</span><span class="s">"viewport"</span> <span class="na">content=</span><span class="s">"width=device-width, initial-scale=1"</span><span class="nt">></span>
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"icon"</span> <span class="na">type=</span><span class="s">"image/x-icon"</span> <span class="na">href=</span><span class="s">"favicon.ico"</span><span class="nt">></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">></span>
<span class="nt"><app-root></app-root></span>
<span class="nt"></div></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<p>Run the server again to view the updated styles:</p>
<p><img src="/assets/img/blog/angular-auth-ngrx/bootstrap-welcome.png" alt="bootstrap welcome" /></p>
<h2 id="routing-and-components">Routing and Components</h2>
<p>Next, let’s add three components and configure some basic routes:</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Purpose</th>
<th>Route</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="highlighter-rouge">Landing</code></td>
<td>Landing page</td>
<td><code class="highlighter-rouge">/</code></td>
</tr>
<tr>
<td><code class="highlighter-rouge">LogIn</code></td>
<td>Authenticating existing users</td>
<td><code class="highlighter-rouge">/log-in</code></td>
</tr>
<tr>
<td><code class="highlighter-rouge">SignUp</code></td>
<td>Registering new users</td>
<td><code class="highlighter-rouge">/sign-up</code></td>
</tr>
</tbody>
</table>
<p>Start by generating the components:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ng generate component components/landing
<span class="nv">$ </span>ng generate component components/sign-up
<span class="nv">$ </span>ng generate component components/log-in
</code></pre></div></div>
<p>Next, add the <a href="https://angular.io/api/router/RouterModule">RouterModule</a> import to the <em>app.module.ts</em> file:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">RouterModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/router'</span><span class="p">;</span>
</code></pre></div></div>
<p>Add the following route configuration, to the <code class="highlighter-rouge">imports</code> array, to map each of the components we just created to a URL:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">RouterModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">([</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'log-in'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LogInComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'sign-up'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">SignUpComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LandingComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'**'</span><span class="p">,</span> <span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span> <span class="p">}</span>
<span class="p">])</span>
</code></pre></div></div>
<p>Your <em>app.module.ts</em> should now look like:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">BrowserModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/platform-browser'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">NgModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">RouterModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/router'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./app.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LandingComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/landing/landing.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">SignUpComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/sign-up/sign-up.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LogInComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/log-in/log-in.component'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">NgModule</span><span class="p">({</span>
<span class="na">declarations</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">AppComponent</span><span class="p">,</span>
<span class="nx">LandingComponent</span><span class="p">,</span>
<span class="nx">SignUpComponent</span><span class="p">,</span>
<span class="nx">LogInComponent</span>
<span class="p">],</span>
<span class="na">imports</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">BrowserModule</span><span class="p">,</span>
<span class="nx">RouterModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">([</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'log-in'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LogInComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'sign-up'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">SignUpComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LandingComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'**'</span><span class="p">,</span> <span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span> <span class="p">}</span>
<span class="p">])</span>
<span class="p">],</span>
<span class="na">providers</span><span class="p">:</span> <span class="p">[],</span>
<span class="na">bootstrap</span><span class="p">:</span> <span class="p">[</span><span class="nx">AppComponent</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">AppModule</span> <span class="p">{</span> <span class="p">}</span>
</code></pre></div></div>
<p>Fire up the server and test each URL in the browser:</p>
<ol>
<li><a href="http://localhost:4200">http://localhost:4200</a></li>
<li><a href="http://localhost:4200/log-in">http://localhost:4200/log-in</a></li>
<li><a href="http://localhost:4200/sign-up">http://localhost:4200/sign-up</a></li>
<li><a href="http://localhost:4200/notreal">http://localhost:4200/notreal</a></li>
</ol>
<p>We know that the routes are configured correctly since <code class="highlighter-rouge">/notreal</code> redirects to <code class="highlighter-rouge">/</code> (and renders the <code class="highlighter-rouge">LandingComponent</code>) while <code class="highlighter-rouge">/log-in</code> and <code class="highlighter-rouge">/sign-up</code> work just fine. Now, we still need to use the <code class="highlighter-rouge">RouterOutlet</code> <a href="https://angular.io/api/router/RouterOutlet">directive</a> to tell Angular where to insert each of our HTML templates.</p>
<p>Replace the contents of the <em>app.component.html</em> file with the <code class="highlighter-rouge"><router-outlet></code> tag:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><router-outlet></router-outlet></span>
</code></pre></div></div>
<p>Run the app again. Make sure you are shown the correct component when you visit each of the URLs in the browser.</p>
<p>Finally, update <em>src/app/components/landing/landing.component.html</em>:</p>
<pre><code class="language-ng2"><div class="row">
<div class="col-md-4">
<h1>Angular + NGRX</h1>
<hr><br>
<a [routerLink]="['/log-in']" class="btn btn-primary">Log in</a>
<a [routerLink]="['/sign-up']" class="btn btn-primary">Sign up</a>
</div>
</div>
</code></pre>
<div style="text-align:left;padding-top:10px;padding-left:10px;">
<img src="/assets/img/blog/angular-auth-ngrx/landing.png" style="max-width: 70%;border:0;box-shadow:none;" alt="landing component" />
</div>
<p>And, with that, we’re ready to start adding authentication!</p>
<h2 id="workflow">Workflow</h2>
<h3 id="configure-forms">Configure Forms</h3>
<ol>
<li>Add form to the <code class="highlighter-rouge">SignUpComponent</code></li>
<li>Define the <code class="highlighter-rouge">User</code> Model</li>
<li>Add form to the <code class="highlighter-rouge">LogInComponent</code></li>
</ol>
<h3 id="ngrx-store">NGRX Store</h3>
<ol>
<li>Install NGRX Store</li>
<li>Add files and folders for the actions and reducers</li>
<li>Define the state</li>
</ol>
<h3 id="configure-auth-service">Configure Auth Service</h3>
<ol>
<li>Spin up the fake back-end server</li>
<li>Add a service</li>
</ol>
<h3 id="ngrx-effects">NGRX Effects</h3>
<ol>
<li>Install NGRX Effects</li>
<li>Add effects file</li>
</ol>
<h3 id="configure-login">Configure Login</h3>
<ol>
<li>Login
<ul>
<li>Dispatch <code class="highlighter-rouge">LogIn</code> action</li>
<li>Add action</li>
<li>Add effect (to dispatch either <code class="highlighter-rouge">LogInSuccess</code> or <code class="highlighter-rouge">LogInFailure</code>)</li>
</ul>
</li>
<li>Login Success
<ul>
<li>Add action</li>
<li>Add reducer (to create new state)</li>
<li>Add effect (to add token to localStorage and redirect user)</li>
</ul>
</li>
<li>Login Failure
<ul>
<li>Add action</li>
<li>Add reducer (to create new state)</li>
<li>Add effect</li>
</ul>
</li>
</ol>
<h3 id="configure-signup">Configure Signup</h3>
<ol>
<li>Signup
<ul>
<li>Dispatch <code class="highlighter-rouge">SignUp</code> action</li>
<li>Add action</li>
<li>Add effect (to dispatch either <code class="highlighter-rouge">SignUpSuccess</code> or <code class="highlighter-rouge">SignUpFailure</code>)</li>
</ul>
</li>
<li>Signup Success
<ul>
<li>Add action</li>
<li>Add reducer (to create new state)</li>
<li>Add effect (to add token to localStorage and redirect user)</li>
</ul>
</li>
<li>Signup Failure
<ul>
<li>Add action</li>
<li>Add reducer (to create new state)</li>
<li>Add effect</li>
</ul>
</li>
</ol>
<h3 id="configure-logout">Configure Logout</h3>
<ol>
<li>Logout
<ul>
<li>Dispatch <code class="highlighter-rouge">LogIn</code> action</li>
<li>Add action</li>
<li>Add reducer (to create new state)</li>
<li>Add effect (to remove token from localStorage)</li>
</ul>
</li>
</ol>
<h3 id="update-the-templates">Update the Templates</h3>
<ol>
<li>Add error messages to the forms
<ul>
<li>Update the components</li>
<li>Add messages to the templates</li>
</ul>
</li>
<li>Update the <code class="highlighter-rouge">LandingComponent</code></li>
</ol>
<h3 id="add-http-interceptor">Add HTTP Interceptor</h3>
<ol>
<li>Configure the interceptor</li>
<li>Handle unauthorized responses</li>
<li>Add new route</li>
</ol>
<h3 id="route-guard">Route Guard</h3>
<ol>
<li>Configure the interface</li>
<li>Protect the route</li>
</ol>
<p>Let’s get to it!</p>
<h2 id="configure-forms-1">Configure Forms</h2>
<h3 id="add-form-to-the-signupcomponent">Add form to the <code class="highlighter-rouge">SignUpComponent</code></h3>
<p>Start with the template:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><div class="row">
<div class="col-md-4">
<h1>Sign up</h1>
<hr><br>
<form (ngSubmit)="onSubmit()" ngNativeValidate>
<div class="form-group">
<label for="email">Email</label>
<input
[(ngModel)]="user.email"
name="email"
type="email"
required
class="form-control"
id="email"
placeholder="enter your email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input
[(ngModel)]="user.password"
name="password"
type="password"
required
class="form-control"
id="password"
placeholder="enter a password">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
<a [routerLink]="['/']" class="btn btn-success">Cancel</a>
</form>
<p>
<span>Already have an account?&nbsp;</span>
<a [routerLink]="['/log-in']">Log in!</a>
</p>
</div>
</div>
</code></pre></div></div>
<p>Since Angular now automatically adds a <code class="highlighter-rouge">novalidate</code> attribute to forms, we used the <code class="highlighter-rouge">ngNativeValidate</code> <a href="https://github.com/angular/angular/blob/master/packages/forms/src/directives/ng_no_validate_directive.ts">directive</a> to turn the browser’s native form validation back on.</p>
<blockquote>
<p>Feel free to use Angular’s <a href="https://angular.io/guide/form-validation">form validation</a>.</p>
</blockquote>
<p>Then, wire up the component itself:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">OnInit</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../models/user'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="na">selector</span><span class="p">:</span> <span class="s1">'app-sign-up'</span><span class="p">,</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'./sign-up.component.html'</span><span class="p">,</span>
<span class="na">styleUrls</span><span class="p">:</span> <span class="p">[</span><span class="s1">'./sign-up.component.css'</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUpComponent</span> <span class="kr">implements</span> <span class="nx">OnInit</span> <span class="p">{</span>
<span class="nl">user</span><span class="p">:</span> <span class="nx">User</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">User</span><span class="p">();</span>
<span class="kd">constructor</span><span class="p">()</span> <span class="p">{</span> <span class="p">}</span>
<span class="nx">ngOnInit</span><span class="p">()</span> <span class="p">{</span>
<span class="p">}</span>
<span class="nx">onSubmit</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">user</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Add the <code class="highlighter-rouge">FormsModule</code> to <em>app.module.ts</em>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">BrowserModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/platform-browser'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">NgModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">RouterModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/router'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">FormsModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/forms'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./app.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LandingComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/landing/landing.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">SignUpComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/sign-up/sign-up.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LogInComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/log-in/log-in.component'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">NgModule</span><span class="p">({</span>
<span class="na">declarations</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">AppComponent</span><span class="p">,</span>
<span class="nx">LandingComponent</span><span class="p">,</span>
<span class="nx">SignUpComponent</span><span class="p">,</span>
<span class="nx">LogInComponent</span>
<span class="p">],</span>
<span class="na">imports</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">BrowserModule</span><span class="p">,</span>
<span class="nx">FormsModule</span><span class="p">,</span>
<span class="nx">RouterModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">([</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'log-in'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LogInComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'sign-up'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">SignUpComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LandingComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'**'</span><span class="p">,</span> <span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span> <span class="p">}</span>
<span class="p">])</span>
<span class="p">],</span>
<span class="na">providers</span><span class="p">:</span> <span class="p">[],</span>
<span class="na">bootstrap</span><span class="p">:</span> <span class="p">[</span><span class="nx">AppComponent</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">AppModule</span> <span class="p">{</span> <span class="p">}</span>
</code></pre></div></div>
<p>Run the application. The app should fail to build, and you should see the following error in the terminal since the <code class="highlighter-rouge">User</code> model does not exist:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ERROR <span class="k">in </span>src/app/components/sign-up/sign-up.component.ts<span class="o">(</span>3,22<span class="o">)</span>: error TS2307:
Cannot find module <span class="s1">'../../models/user'</span><span class="nb">.</span>
</code></pre></div></div>
<p>Kill the server.</p>
<h3 id="define-the-user-model">Define the <code class="highlighter-rouge">User</code> Model</h3>
<p>Moving along, let’s generate a new model class:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ng generate class models/user
</code></pre></div></div>
<p>Then update <em>src/app/models/user.ts</em>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">class</span> <span class="nx">User</span> <span class="p">{</span>
<span class="nx">id</span><span class="p">?:</span> <span class="nx">string</span><span class="p">;</span>
<span class="nx">email</span><span class="p">?:</span> <span class="nx">string</span><span class="p">;</span>
<span class="nx">password</span><span class="p">?:</span> <span class="nx">string</span><span class="p">;</span>
<span class="nx">token</span><span class="p">?:</span> <span class="nx">string</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Run the server again. The app should compile just fine and the terminal should be error-free. Try submitting the form. You should see the user model logged to the JavaScript Console:</p>
<div style="text-align:left;padding-top:10px;padding-left:10px;">
<img src="/assets/img/blog/angular-auth-ngrx/signup.png" style="max-width: 70%;border:0;box-shadow:none;" alt="signup component" />
</div>
<h3 id="add-form-to-the-logincomponent">Add form to the <code class="highlighter-rouge">LogInComponent</code></h3>
<p>Again, start with the template:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><div class="row">
<div class="col-md-4">
<h1>Log in</h1>
<hr><br>
<form (ngSubmit)="onSubmit()" ngNativeValidate>
<div class="form-group">
<label for="email">Email</label>
<input
[(ngModel)]="user.email"
name="email"
type="email"
required
class="form-control"
id="email"
placeholder="enter your email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input
[(ngModel)]="user.password"
name="password"
type="password"
required
class="form-control"
id="password"
placeholder="enter a password">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
<a [routerLink]="['/']" class="btn btn-success">Cancel</a>
</form>
<p>
<span>Don't have an account?&nbsp;</span>
<a [routerLink]="['/sign-up']">Sign up!</a>
</p>
</div>
</div>
</code></pre></div></div>
<p>Then, update the <code class="highlighter-rouge">LogInComponent</code> itself:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">OnInit</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../models/user'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="na">selector</span><span class="p">:</span> <span class="s1">'app-log-in'</span><span class="p">,</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'./log-in.component.html'</span><span class="p">,</span>
<span class="na">styleUrls</span><span class="p">:</span> <span class="p">[</span><span class="s1">'./log-in.component.css'</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInComponent</span> <span class="kr">implements</span> <span class="nx">OnInit</span> <span class="p">{</span>
<span class="nl">user</span><span class="p">:</span> <span class="nx">User</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">User</span><span class="p">();</span>
<span class="kd">constructor</span><span class="p">()</span> <span class="p">{</span> <span class="p">}</span>
<span class="nx">ngOnInit</span><span class="p">()</span> <span class="p">{</span>
<span class="p">}</span>
<span class="nx">onSubmit</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">user</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Test it out! Like the sign up form, you should see the user model logged to the JavaScript Console.</p>
<p>With that, let’s move on to NGRX!</p>
<h2 id="ngrx-store-1">NGRX Store</h2>
<h3 id="install-ngrx-store">Install NGRX Store</h3>
<p>State management is difficult. As your application scales, state generally scatters across your application, tucked away in various nooks and crannies. Although it’s not an issue yet, it’s a good idea to set a solid foundation to help ensure that, going forward, state management is easier and more predictable. This is where <a href="https://ngrx.io/guide/store">NGRX Store</a> comes into play. It helps to solve this problem by managing state in a single, immutable data store.</p>
<p>Core tenets</p>
<ol>
<li>State is a single immutable data structure</li>
<li>Actions describe state changes</li>
<li>Pure functions called reducers take the previous state and the next action to compute the new state</li>
<li>State accessed with the Store, an observable of state and an observer of actions</li>
</ol>
<p>In a nutshell, NGRX Store builds on Redux’s core patterns by adding in RxJS. It’s specifically designed for Angular apps.</p>
<p>Building blocks:</p>
<ol>
<li><em>Store</em> - single, immutable data structure</li>
<li><em>Actions</em> - describe changes to state</li>
<li><em>Reducers</em> - pure functions that create a new state</li>
</ol>
<p>Example:</p>
<div style="text-align:left;padding-top:10px;padding-left:10px;padding-bottom:20px;">
<img src="/assets/img/blog/angular-auth-ngrx/angular-ngrx-store-flow.png" style="max-width:90%;border:0;box-shadow:none;" alt="angular ngrx store flow" />
</div>
<p>Install:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install @ngrx/store@5.2.0 <span class="nt">--save</span>
</code></pre></div></div>
<h3 id="add-files-and-folders-for-the-actions-and-reducers">Add files and folders for the actions and reducers</h3>
<p>Next, we need to add a bit of structure for the actions and reducers. Within “src/app”, add a new folder called “store”. Then, add the following folders to “store”:</p>
<ol>
<li>“actions”</li>
<li>“reducers”</li>
</ol>
<p>Add a file called <em>app.states.ts</em> as well. Finally, add a single file to each folder:</p>
<ol>
<li><em>auth.actions.ts</em></li>
<li><em>auth.reducers.ts</em></li>
</ol>
<p>You should now have:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>└── store
├── actions
│ └── auth.actions.ts
├── app.states.ts
└── reducers
└── auth.reducers.ts
</code></pre></div></div>
<blockquote>
<p>You could also group actions and reducers by domain. Actions and reducers would live at the component level, in other words. If you decide to go that route, you should probably create a “common” folder for actions and reducers that are used across a number of components. Auth actions and reducers should then live in “common”.</p>
</blockquote>
<h3 id="define-the-state">Define the state</h3>
<p>Before creating any actions or reducers, let’s define structure of the store in <em>src/app/store/reducers/auth.reducers.ts</em>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../models/user'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">State</span> <span class="p">{</span>
<span class="c1">// is a user authenticated?</span>
<span class="nl">isAuthenticated</span><span class="p">:</span> <span class="kr">boolean</span><span class="p">;</span>
<span class="c1">// if authenticated, there should be a user object</span>
<span class="nl">user</span><span class="p">:</span> <span class="nx">User</span> <span class="o">|</span> <span class="kc">null</span><span class="p">;</span>
<span class="c1">// error message</span>
<span class="nl">errorMessage</span><span class="p">:</span> <span class="nx">string</span> <span class="o">|</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<blockquote>
<p>Remember: State is a single, immutable data structure.</p>
</blockquote>
<p>Also, add an <code class="highlighter-rouge">initialState</code> object:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">initialState</span><span class="p">:</span> <span class="nx">State</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">isAuthenticated</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="na">user</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="na">errorMessage</span><span class="p">:</span> <span class="kc">null</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Then, define the top-level state interface in <em>src/app/store/app.states.ts</em>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">auth</span> <span class="k">from</span> <span class="s1">'./reducers/auth.reducers'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">AppState</span> <span class="p">{</span>
<span class="nl">authState</span><span class="p">:</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">State</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This is a map of keys to the inner state types.</p>
<h2 id="configure-auth-service-1">Configure Auth Service</h2>
<h3 id="spin-up-the-fake-back-end-server">Spin up the fake back-end server</h3>
<p>In this section we’ll spin up a fake back-end that the client can communicate with. The app itself is a basic Node/Express application with the following routes:</p>
<table>
<thead>
<tr>
<th>URL</th>
<th>HTTP Verb</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>http://localhost:1337/ping</td>
<td>GET</td>
<td>Sanity Check</td>
</tr>
<tr>
<td>http://localhost:1337/register</td>
<td>POST</td>
<td>Register a new user</td>
</tr>
<tr>
<td>http://localhost:1337/login</td>
<td>POST</td>
<td>Log a user in</td>
</tr>
<tr>
<td>http://localhost:1337/status</td>
<td>GET</td>
<td>Get user status</td>
</tr>
</tbody>
</table>
<blockquote>
<p>Just keep in mind that the back-end does <strong>not</strong> create a real <a href="https://en.wikipedia.org/wiki/JSON_Web_Token">JSON Web Token</a> (JWT). Feel free to swap it out for a working back-end or use the final application from the <a href="http://mherman.org/blog/2016/10/28/token-based-authentication-with-node">Token-Based Authentication with Node</a> blog post, if you’d like.</p>
</blockquote>
<p>Within a new terminal window, clone down the repo, install the dependencies, and spin up the app:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/testdrivenio/fake-token-api
<span class="nv">$ </span><span class="nb">cd </span>fake-token-api
<span class="nv">$ </span>npm install
<span class="nv">$ </span>npm start
</code></pre></div></div>
<p>In your browser, <a href="http://localhost:1337/ping">http://localhost:1337/ping</a> should return <code class="highlighter-rouge">"pong!"</code>.</p>
<h3 id="add-the-service">Add the service</h3>
<p>Back in Angular land, we need to wire up a service that’s responsible for making API calls to the fake back-end:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ng generate service services/auth
</code></pre></div></div>
<p>Add the service as a provider in the <code class="highlighter-rouge">@NgModule</code> definition and import the <code class="highlighter-rouge">HttpClientModule</code>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">BrowserModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/platform-browser'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">NgModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">RouterModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/router'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">FormsModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/forms'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">HttpClientModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/common/http'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./app.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LandingComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/landing/landing.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">SignUpComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/sign-up/sign-up.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LogInComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/log-in/log-in.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AuthService</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./services/auth.service'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">NgModule</span><span class="p">({</span>
<span class="na">declarations</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">AppComponent</span><span class="p">,</span>
<span class="nx">LandingComponent</span><span class="p">,</span>
<span class="nx">SignUpComponent</span><span class="p">,</span>
<span class="nx">LogInComponent</span>
<span class="p">],</span>
<span class="na">imports</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">BrowserModule</span><span class="p">,</span>
<span class="nx">FormsModule</span><span class="p">,</span>
<span class="nx">HttpClientModule</span><span class="p">,</span>
<span class="nx">RouterModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">([</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'log-in'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LogInComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'sign-up'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">SignUpComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LandingComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'**'</span><span class="p">,</span> <span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span> <span class="p">}</span>
<span class="p">])</span>
<span class="p">],</span>
<span class="na">providers</span><span class="p">:</span> <span class="p">[</span><span class="nx">AuthService</span><span class="p">],</span>
<span class="na">bootstrap</span><span class="p">:</span> <span class="p">[</span><span class="nx">AppComponent</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">AppModule</span> <span class="p">{</span> <span class="p">}</span>
</code></pre></div></div>
<p>Add the following methods to the service:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">HttpClient</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/common/http'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Injectable</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Observable</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'rxjs/Observable'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../models/user'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">AuthService</span> <span class="p">{</span>
<span class="kr">private</span> <span class="nx">BASE_URL</span> <span class="o">=</span> <span class="s1">'http://localhost:1337'</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">private</span> <span class="nx">http</span><span class="p">:</span> <span class="nx">HttpClient</span><span class="p">)</span> <span class="p">{}</span>
<span class="nx">getToken</span><span class="p">():</span> <span class="nx">string</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">logIn</span><span class="p">(</span><span class="nx">email</span><span class="p">:</span> <span class="nx">string</span><span class="p">,</span> <span class="nx">password</span><span class="p">:</span> <span class="nx">string</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">BASE_URL</span><span class="p">}</span><span class="s2">/login`</span><span class="p">;</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">http</span><span class="p">.</span><span class="nx">post</span><span class="o"><</span><span class="nx">User</span><span class="o">></span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span><span class="nx">email</span><span class="p">,</span> <span class="nx">password</span><span class="p">});</span>
<span class="p">}</span>
<span class="nx">signUp</span><span class="p">(</span><span class="na">email</span><span class="p">:</span> <span class="nx">string</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="nx">string</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">User</span><span class="o">></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">BASE_URL</span><span class="p">}</span><span class="s2">/register`</span><span class="p">;</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">http</span><span class="p">.</span><span class="nx">post</span><span class="o"><</span><span class="nx">User</span><span class="o">></span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span><span class="nx">email</span><span class="p">,</span> <span class="nx">password</span><span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Both the <code class="highlighter-rouge">logIn</code> and <code class="highlighter-rouge">signUp</code> methods return <code class="highlighter-rouge">Observable</code>s and create new <code class="highlighter-rouge">User</code>s. We’ll need to subscribe to <code class="highlighter-rouge">logIn</code> and <code class="highlighter-rouge">signUp</code>, after a successful form submission, to send the respective HTTP requests within the effects module.</p>
<h2 id="ngrx-effects-1">NGRX Effects</h2>
<h3 id="install-ngrx-effects">Install NGRX Effects</h3>
<p><a href="https://ngrx.io/guide/effects">NGRX Effects</a> listen for actions dispatched from the NGRX Store, perform some logic (e.g., a side effect), and then dispatch a new action.</p>
<div style="text-align:left;padding-top:10px;padding-left:10px;padding-bottom:20px;">
<img src="/assets/img/blog/angular-auth-ngrx/angular-ngrx-store-effects-flow.png" style="max-width:90%;border:0;box-shadow:none;" alt="angular ngrx store + effects flow" />
</div>
<p>Install:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install @ngrx/effects@5.2.0 <span class="nt">--save</span>
</code></pre></div></div>
<h3 id="add-effects-file">Add effects file</h3>
<p>Then, within the “store” folder, add a new folder called “effects”. And, within that folder, add a new file called <em>auth.effects.ts</em>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Injectable</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Router</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/router'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Actions</span><span class="p">,</span> <span class="nx">Effect</span><span class="p">,</span> <span class="nx">ofType</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/effects'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Observable</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'rxjs/Observable'</span><span class="p">;</span>
<span class="k">import</span> <span class="s1">'rxjs/add/observable/of'</span><span class="p">;</span>
<span class="k">import</span> <span class="s1">'rxjs/add/operator/map'</span><span class="p">;</span>
<span class="k">import</span> <span class="s1">'rxjs/add/operator/switchMap'</span><span class="p">;</span>
<span class="k">import</span> <span class="s1">'rxjs/add/operator/catch'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">tap</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'rxjs/operators'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AuthService</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../services/auth.service'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">AuthEffects</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span>
<span class="kr">private</span> <span class="nx">actions</span><span class="p">:</span> <span class="nx">Actions</span><span class="p">,</span>
<span class="kr">private</span> <span class="nx">authService</span><span class="p">:</span> <span class="nx">AuthService</span><span class="p">,</span>
<span class="kr">private</span> <span class="nx">router</span><span class="p">:</span> <span class="nx">Router</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{}</span>
<span class="c1">// effects go here</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Then, register the effects module in <code class="highlighter-rouge">@NgModule</code>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">BrowserModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/platform-browser'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">NgModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">RouterModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/router'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">FormsModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/forms'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">HttpClientModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/common/http'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">EffectsModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/effects'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./app.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LandingComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/landing/landing.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">SignUpComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/sign-up/sign-up.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LogInComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/log-in/log-in.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AuthService</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./services/auth.service'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AuthEffects</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./store/effects/auth.effects'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">NgModule</span><span class="p">({</span>
<span class="na">declarations</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">AppComponent</span><span class="p">,</span>
<span class="nx">LandingComponent</span><span class="p">,</span>
<span class="nx">SignUpComponent</span><span class="p">,</span>
<span class="nx">LogInComponent</span>
<span class="p">],</span>
<span class="na">imports</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">BrowserModule</span><span class="p">,</span>
<span class="nx">FormsModule</span><span class="p">,</span>
<span class="nx">HttpClientModule</span><span class="p">,</span>
<span class="nx">EffectsModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">([</span><span class="nx">AuthEffects</span><span class="p">]),</span>
<span class="nx">RouterModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">([</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'log-in'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LogInComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'sign-up'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">SignUpComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LandingComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'**'</span><span class="p">,</span> <span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span> <span class="p">}</span>
<span class="p">])</span>
<span class="p">],</span>
<span class="na">providers</span><span class="p">:</span> <span class="p">[</span><span class="nx">AuthService</span><span class="p">],</span>
<span class="na">bootstrap</span><span class="p">:</span> <span class="p">[</span><span class="nx">AppComponent</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">AppModule</span> <span class="p">{</span> <span class="p">}</span>
</code></pre></div></div>
<h2 id="configure-login-1">Configure Login</h2>
<h3 id="login">Login</h3>
<h4 id="dispatch-login-action">Dispatch <code class="highlighter-rouge">LogIn</code> action</h4>
<p>First, after a successful form submission, we need to dispatch a <code class="highlighter-rouge">LogIn</code> action, which will send an action and a parameter to a reducer to eventually create a new state.</p>
<p><em>src/app/components/log-in/log-in.component.ts</em>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">OnInit</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Store</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../models/user'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppState</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/app.states'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LogIn</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/actions/auth.actions'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="na">selector</span><span class="p">:</span> <span class="s1">'app-log-in'</span><span class="p">,</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'./log-in.component.html'</span><span class="p">,</span>
<span class="na">styleUrls</span><span class="p">:</span> <span class="p">[</span><span class="s1">'./log-in.component.css'</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInComponent</span> <span class="kr">implements</span> <span class="nx">OnInit</span> <span class="p">{</span>
<span class="nl">user</span><span class="p">:</span> <span class="nx">User</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">User</span><span class="p">();</span>
<span class="kd">constructor</span><span class="p">(</span>
<span class="kr">private</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">Store</span><span class="o"><</span><span class="nx">AppState</span><span class="o">></span>
<span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
<span class="nx">ngOnInit</span><span class="p">()</span> <span class="p">{</span>
<span class="p">}</span>
<span class="nx">onSubmit</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">email</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">password</span>
<span class="p">};</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">(</span><span class="k">new</span> <span class="nx">LogIn</span><span class="p">(</span><span class="nx">payload</span><span class="p">));</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>With the Angular server running, you should see the following error since we still need to set up the module for the actions along with the <code class="highlighter-rouge">LogIn</code> action:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ERROR <span class="k">in </span>src/app/components/log-in/log-in.component.ts<span class="o">(</span>6,23<span class="o">)</span>: error TS2306:
File <span class="s1">'angular-auth-ngrx/src/app/store/actions/auth.actions.ts'</span> is not a module.
</code></pre></div></div>
<h4 id="add-action">Add action</h4>
<p>Actions describe changes to state. They are dispatched to a reducer, which will then create a new state.</p>
<p>Update <em>src/app/store/user.actions.ts</em> like so:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">enum</span> <span class="nx">AuthActionTypes</span> <span class="p">{</span>
<span class="nx">LOGIN</span> <span class="o">=</span> <span class="s1">'[Auth] Login'</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Add the action class as well:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">class</span> <span class="nx">LogIn</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="nx">type</span> <span class="nx">All</span> <span class="o">=</span>
<span class="o">|</span> <span class="nx">LogIn</span><span class="p">;</span>
</code></pre></div></div>
<p>You should now have:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">enum</span> <span class="nx">AuthActionTypes</span> <span class="p">{</span>
<span class="nx">LOGIN</span> <span class="o">=</span> <span class="s1">'[Auth] Login'</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogIn</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="nx">type</span> <span class="nx">All</span> <span class="o">=</span>
<span class="o">|</span> <span class="nx">LogIn</span><span class="p">;</span>
</code></pre></div></div>
<h4 id="add-effect-to-dispatch-either-loginsuccess-or-loginfailure">Add effect (to dispatch either <code class="highlighter-rouge">LogInSuccess</code> or <code class="highlighter-rouge">LogInFailure</code>)</h4>
<p>Add the first effect to <em>src/app/store/effects/auth.effects.ts</em></p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Effect</span><span class="p">()</span>
<span class="nx">LogIn</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">actions</span>
<span class="p">.</span><span class="nx">ofType</span><span class="p">(</span><span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN</span><span class="p">)</span>
<span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">action</span><span class="p">:</span> <span class="nx">LogIn</span><span class="p">)</span> <span class="o">=></span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">)</span>
<span class="p">.</span><span class="nx">switchMap</span><span class="p">(</span><span class="nx">payload</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">authService</span><span class="p">.</span><span class="nx">logIn</span><span class="p">(</span><span class="nx">payload</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">password</span><span class="p">)</span>
<span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">LogInSuccess</span><span class="p">({</span><span class="na">token</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">token</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">email</span><span class="p">});</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">Observable</span><span class="p">.</span><span class="k">of</span><span class="p">(</span><span class="k">new</span> <span class="nx">LogInFailure</span><span class="p">({</span> <span class="na">error</span><span class="p">:</span> <span class="nx">error</span> <span class="p">}));</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>The <code class="highlighter-rouge">ofType</code> operator filters the action by a type. It accepts multiple action types, so one effect can handle a number of actions. Then, with <code class="highlighter-rouge">map</code>, we “map” the action to its payload. This, essentially, returns an observable with <em>just</em> the payload. The <code class="highlighter-rouge">switchMap</code> is used to switch <em>back</em> to the response observable but still use the payload as an argument in the <code class="highlighter-rouge">switchMap</code> function.</p>
<p>Add the import:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span>
<span class="nx">AuthActionTypes</span><span class="p">,</span>
<span class="nx">LogIn</span><span class="p">,</span> <span class="nx">LogInSuccess</span><span class="p">,</span> <span class="nx">LogInFailure</span><span class="p">,</span>
<span class="p">}</span> <span class="k">from</span> <span class="s1">'../actions/auth.actions'</span><span class="p">;</span>
</code></pre></div></div>
<p>For AJAX requests, it’s a good practice to also dispatch a success or error action based on the result of the request.</p>
<h3 id="login-success">Login Success</h3>
<h4 id="add-action-1">Add action</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">enum</span> <span class="nx">AuthActionTypes</span> <span class="p">{</span>
<span class="nx">LOGIN</span> <span class="o">=</span> <span class="s1">'[Auth] Login'</span><span class="p">,</span>
<span class="nx">LOGIN_SUCCESS</span> <span class="o">=</span> <span class="s1">'[Auth] Login Success'</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogIn</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInSuccess</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_SUCCESS</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="nx">type</span> <span class="nx">All</span> <span class="o">=</span>
<span class="o">|</span> <span class="nx">LogIn</span>
<span class="o">|</span> <span class="nx">LogInSuccess</span><span class="p">;</span>
</code></pre></div></div>
<h4 id="add-reducer-to-create-new-state">Add reducer (to create new state)</h4>
<p>Reducers are pure functions that create a new state.</p>
<p>When the log in is successful, we need to set <code class="highlighter-rouge">isAuthenticated</code> to <code class="highlighter-rouge">true</code> and add the token and email to the <code class="highlighter-rouge">user</code> object.</p>
<p>Update <em>src/app/store/reducers/auth.reducers.ts</em>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">function</span> <span class="nx">reducer</span><span class="p">(</span><span class="nx">state</span> <span class="o">=</span> <span class="nx">initialState</span><span class="p">,</span> <span class="nx">action</span><span class="p">:</span> <span class="nx">All</span><span class="p">):</span> <span class="nx">State</span> <span class="p">{</span>
<span class="k">switch</span> <span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span> <span class="p">{</span>
<span class="k">case</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_SUCCESS</span><span class="p">:</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
<span class="p">...</span><span class="nx">state</span><span class="p">,</span>
<span class="na">isAuthenticated</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">user</span><span class="p">:</span> <span class="p">{</span>
<span class="na">token</span><span class="p">:</span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">token</span><span class="p">,</span>
<span class="na">email</span><span class="p">:</span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">email</span>
<span class="p">},</span>
<span class="na">errorMessage</span><span class="p">:</span> <span class="kc">null</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="nl">default</span><span class="p">:</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">state</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Add the import:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">AuthActionTypes</span><span class="p">,</span> <span class="nx">All</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../actions/auth.actions'</span><span class="p">;</span>
</code></pre></div></div>
<p>Import the reducers into the Angular module:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">BrowserModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/platform-browser'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">NgModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">RouterModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/router'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">FormsModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/forms'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">HttpClientModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/common/http'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">EffectsModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/effects'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">StoreModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./app.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LandingComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/landing/landing.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">SignUpComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/sign-up/sign-up.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LogInComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/log-in/log-in.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AuthService</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./services/auth.service'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AuthEffects</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./store/effects/auth.effects'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">reducers</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./store/app.states'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">NgModule</span><span class="p">({</span>
<span class="na">declarations</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">AppComponent</span><span class="p">,</span>
<span class="nx">LandingComponent</span><span class="p">,</span>
<span class="nx">SignUpComponent</span><span class="p">,</span>
<span class="nx">LogInComponent</span>
<span class="p">],</span>
<span class="na">imports</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">BrowserModule</span><span class="p">,</span>
<span class="nx">FormsModule</span><span class="p">,</span>
<span class="nx">HttpClientModule</span><span class="p">,</span>
<span class="nx">StoreModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">(</span><span class="nx">reducers</span><span class="p">,</span> <span class="p">{}),</span>
<span class="nx">EffectsModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">([</span><span class="nx">AuthEffects</span><span class="p">]),</span>
<span class="nx">RouterModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">([</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'log-in'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LogInComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'sign-up'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">SignUpComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LandingComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'**'</span><span class="p">,</span> <span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span> <span class="p">}</span>
<span class="p">])</span>
<span class="p">],</span>
<span class="na">providers</span><span class="p">:</span> <span class="p">[</span><span class="nx">AuthService</span><span class="p">],</span>
<span class="na">bootstrap</span><span class="p">:</span> <span class="p">[</span><span class="nx">AppComponent</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">AppModule</span> <span class="p">{</span> <span class="p">}</span>
</code></pre></div></div>
<p>Then, update <em>app.states.ts</em>, adding in the reducers:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">auth</span> <span class="k">from</span> <span class="s1">'./reducers/auth.reducers'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">AppState</span> <span class="p">{</span>
<span class="nl">authState</span><span class="p">:</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">State</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">reducers</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">auth</span><span class="p">:</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">reducer</span>
<span class="p">};</span>
</code></pre></div></div>
<h4 id="add-effect-to-add-token-to-localstorage-and-redirect-user">Add effect (to add token to localStorage and redirect user)</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Effect</span><span class="p">({</span> <span class="na">dispatch</span><span class="p">:</span> <span class="kc">false</span> <span class="p">})</span>
<span class="nx">LogInSuccess</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">actions</span><span class="p">.</span><span class="nx">pipe</span><span class="p">(</span>
<span class="nx">ofType</span><span class="p">(</span><span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_SUCCESS</span><span class="p">),</span>
<span class="nx">tap</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">token</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">router</span><span class="p">.</span><span class="nx">navigateByUrl</span><span class="p">(</span><span class="s1">'/'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">);</span>
</code></pre></div></div>
<p>Version 5.5 of RXJS introduced the <code class="highlighter-rouge">pipe</code> <a href="https://rxjs.dev/guide/v6/pipeable-operators">method</a>, which is used to compose a number of functions to act on the observable. Again, <code class="highlighter-rouge">ofType</code> associates the effect with an action while <code class="highlighter-rouge">tap</code> performs a side effect transparently. In other words, it returns an observable identical to the source. In our case, we’re adding the token to localStorage and then redirecting the user to <code class="highlighter-rouge">/</code>.</p>
<blockquote>
<p>Check out the comments for <a href="https://github.com/ReactiveX/rxjs/blob/5.5.5/src/Observable.ts#L305">pipe</a> and <a href="https://github.com/ReactiveX/rxjs/blob/5.5.5/src/operators/tap.ts#L14">tap</a>, respectively, from the source code.</p>
</blockquote>
<h3 id="login-failure">Login Failure</h3>
<h4 id="add-action-2">Add action</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">enum</span> <span class="nx">AuthActionTypes</span> <span class="p">{</span>
<span class="nx">LOGIN</span> <span class="o">=</span> <span class="s1">'[Auth] Login'</span><span class="p">,</span>
<span class="nx">LOGIN_SUCCESS</span> <span class="o">=</span> <span class="s1">'[Auth] Login Success'</span><span class="p">,</span>
<span class="nx">LOGIN_FAILURE</span> <span class="o">=</span> <span class="s1">'[Auth] Login Failure'</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogIn</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInSuccess</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_SUCCESS</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInFailure</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_FAILURE</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="nx">type</span> <span class="nx">All</span> <span class="o">=</span>
<span class="o">|</span> <span class="nx">LogIn</span>
<span class="o">|</span> <span class="nx">LogInSuccess</span>
<span class="o">|</span> <span class="nx">LogInFailure</span><span class="p">;</span>
</code></pre></div></div>
<h4 id="add-reducer-to-create-new-state-1">Add reducer (to create new state)</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_FAILURE</span><span class="p">:</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
<span class="p">...</span><span class="nx">state</span><span class="p">,</span>
<span class="na">errorMessage</span><span class="p">:</span> <span class="s1">'Incorrect email and/or password.'</span>
<span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>
<h4 id="add-effect">Add effect</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Effect</span><span class="p">({</span> <span class="na">dispatch</span><span class="p">:</span> <span class="kc">false</span> <span class="p">})</span>
<span class="nx">LogInFailure</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">actions</span><span class="p">.</span><span class="nx">pipe</span><span class="p">(</span>
<span class="nx">ofType</span><span class="p">(</span><span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_FAILURE</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>
<p>Try it out. Make sure the token is added to localStorage and you are redirected after a successful log in:</p>
<ul>
<li><em>email</em>: <code class="highlighter-rouge">test@test.com</code></li>
<li><em>password</em>: <code class="highlighter-rouge">test</code></li>
</ul>
<div style="text-align:left;padding-top:10px;padding-left:10px;padding-bottom:20px">
<img src="/assets/img/blog/angular-auth-ngrx/angular-ngrx-login.gif" style="max-width: 70%;border:0;box-shadow:none;" alt="log in" />
</div>
<p>The fake back-end will throw a <code class="highlighter-rouge">400</code> error if you use any email other than <code class="highlighter-rouge">test@test.com</code>. As of now, nothing happens on the UI, though. We’ll wire up error messaging shorty.</p>
<blockquote>
<p>Looking for a quick challenge? Refactor the <code class="highlighter-rouge">LogIn</code> effect to use the <code class="highlighter-rouge">pipe</code> method. Clean up the code!</p>
</blockquote>
<h2 id="configure-signup-1">Configure Signup</h2>
<p>Your turn! Configuring the sign up functionality is nearly the same as the log in functionality. Try it on your own before reviewing the post.</p>
<h3 id="signup">Signup</h3>
<h4 id="dispatch-signup-action">Dispatch <code class="highlighter-rouge">SignUp</code> action</h4>
<p><em>src/app/components/sign-up/sign-up.component.ts</em>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">OnInit</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Store</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../models/user'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppState</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/app.states'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">SignUp</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/actions/auth.actions'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="na">selector</span><span class="p">:</span> <span class="s1">'app-sign-up'</span><span class="p">,</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'./sign-up.component.html'</span><span class="p">,</span>
<span class="na">styleUrls</span><span class="p">:</span> <span class="p">[</span><span class="s1">'./sign-up.component.css'</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUpComponent</span> <span class="kr">implements</span> <span class="nx">OnInit</span> <span class="p">{</span>
<span class="nl">user</span><span class="p">:</span> <span class="nx">User</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">User</span><span class="p">();</span>
<span class="kd">constructor</span><span class="p">(</span>
<span class="kr">private</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">Store</span><span class="o"><</span><span class="nx">AppState</span><span class="o">></span>
<span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
<span class="nx">ngOnInit</span><span class="p">()</span> <span class="p">{</span>
<span class="p">}</span>
<span class="nx">onSubmit</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">email</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">password</span>
<span class="p">};</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">(</span><span class="k">new</span> <span class="nx">SignUp</span><span class="p">(</span><span class="nx">payload</span><span class="p">));</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h4 id="add-action-3">Add action</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">enum</span> <span class="nx">AuthActionTypes</span> <span class="p">{</span>
<span class="nx">LOGIN</span> <span class="o">=</span> <span class="s1">'[Auth] Login'</span><span class="p">,</span>
<span class="nx">LOGIN_SUCCESS</span> <span class="o">=</span> <span class="s1">'[Auth] Login Success'</span><span class="p">,</span>
<span class="nx">LOGIN_FAILURE</span> <span class="o">=</span> <span class="s1">'[Auth] Login Failure'</span><span class="p">,</span>
<span class="nx">SIGNUP</span> <span class="o">=</span> <span class="s1">'[Auth] Signup'</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogIn</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInSuccess</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_SUCCESS</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInFailure</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_FAILURE</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUp</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="nx">type</span> <span class="nx">All</span> <span class="o">=</span>
<span class="o">|</span> <span class="nx">LogIn</span>
<span class="o">|</span> <span class="nx">LogInSuccess</span>
<span class="o">|</span> <span class="nx">LogInFailure</span>
<span class="o">|</span> <span class="nx">SignUp</span><span class="p">;</span>
</code></pre></div></div>
<h4 id="add-effect-to-dispatch-either-signupsuccess-or-signupfailure">Add effect (to dispatch either <code class="highlighter-rouge">SignUpSuccess</code> or <code class="highlighter-rouge">SignUpFailure</code>)</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Effect</span><span class="p">()</span>
<span class="nx">SignUp</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">actions</span>
<span class="p">.</span><span class="nx">ofType</span><span class="p">(</span><span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP</span><span class="p">)</span>
<span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">action</span><span class="p">:</span> <span class="nx">SignUp</span><span class="p">)</span> <span class="o">=></span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">)</span>
<span class="p">.</span><span class="nx">switchMap</span><span class="p">(</span><span class="nx">payload</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">authService</span><span class="p">.</span><span class="nx">signUp</span><span class="p">(</span><span class="nx">payload</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">password</span><span class="p">)</span>
<span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">SignUpSuccess</span><span class="p">({</span><span class="na">token</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">token</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">email</span><span class="p">});</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">Observable</span><span class="p">.</span><span class="k">of</span><span class="p">(</span><span class="k">new</span> <span class="nx">SignUpFailure</span><span class="p">({</span> <span class="na">error</span><span class="p">:</span> <span class="nx">error</span> <span class="p">}));</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Add the import:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span>
<span class="nx">AuthActionTypes</span><span class="p">,</span>
<span class="nx">LogIn</span><span class="p">,</span> <span class="nx">LogInSuccess</span><span class="p">,</span> <span class="nx">LogInFailure</span><span class="p">,</span>
<span class="nx">SignUp</span><span class="p">,</span> <span class="nx">SignUpSuccess</span><span class="p">,</span> <span class="nx">SignUpFailure</span>
<span class="p">}</span> <span class="k">from</span> <span class="s1">'../actions/auth.actions'</span><span class="p">;</span>
</code></pre></div></div>
<h3 id="signup-success">Signup Success</h3>
<h4 id="add-action-4">Add action</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">enum</span> <span class="nx">AuthActionTypes</span> <span class="p">{</span>
<span class="nx">LOGIN</span> <span class="o">=</span> <span class="s1">'[Auth] Login'</span><span class="p">,</span>
<span class="nx">LOGIN_SUCCESS</span> <span class="o">=</span> <span class="s1">'[Auth] Login Success'</span><span class="p">,</span>
<span class="nx">LOGIN_FAILURE</span> <span class="o">=</span> <span class="s1">'[Auth] Login Failure'</span><span class="p">,</span>
<span class="nx">SIGNUP</span> <span class="o">=</span> <span class="s1">'[Auth] Signup'</span><span class="p">,</span>
<span class="nx">SIGNUP_SUCCESS</span> <span class="o">=</span> <span class="s1">'[Auth] Signup Success'</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogIn</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInSuccess</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_SUCCESS</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInFailure</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_FAILURE</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUp</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUpSuccess</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_SUCCESS</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="nx">type</span> <span class="nx">All</span> <span class="o">=</span>
<span class="o">|</span> <span class="nx">LogIn</span>
<span class="o">|</span> <span class="nx">LogInSuccess</span>
<span class="o">|</span> <span class="nx">LogInFailure</span>
<span class="o">|</span> <span class="nx">SignUp</span>
<span class="o">|</span> <span class="nx">SignUpSuccess</span><span class="p">;</span>
</code></pre></div></div>
<h4 id="add-reducer-to-create-new-state-2">Add reducer (to create new state)</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_SUCCESS</span><span class="p">:</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
<span class="p">...</span><span class="nx">state</span><span class="p">,</span>
<span class="na">isAuthenticated</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">user</span><span class="p">:</span> <span class="p">{</span>
<span class="na">token</span><span class="p">:</span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">token</span><span class="p">,</span>
<span class="na">email</span><span class="p">:</span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">email</span>
<span class="p">},</span>
<span class="na">errorMessage</span><span class="p">:</span> <span class="kc">null</span>
<span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>
<h4 id="add-effect-to-add-token-to-localstorage-and-redirect-user-1">Add effect (to add token to localStorage and redirect user)</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Effect</span><span class="p">({</span> <span class="na">dispatch</span><span class="p">:</span> <span class="kc">false</span> <span class="p">})</span>
<span class="nx">SignUpSuccess</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">actions</span><span class="p">.</span><span class="nx">pipe</span><span class="p">(</span>
<span class="nx">ofType</span><span class="p">(</span><span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_SUCCESS</span><span class="p">),</span>
<span class="nx">tap</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">token</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">router</span><span class="p">.</span><span class="nx">navigateByUrl</span><span class="p">(</span><span class="s1">'/'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">);</span>
</code></pre></div></div>
<h3 id="signup-failure">Signup Failure</h3>
<p>Again, try this on your own!</p>
<h4 id="add-action-5">Add action</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">enum</span> <span class="nx">AuthActionTypes</span> <span class="p">{</span>
<span class="nx">LOGIN</span> <span class="o">=</span> <span class="s1">'[Auth] Login'</span><span class="p">,</span>
<span class="nx">LOGIN_SUCCESS</span> <span class="o">=</span> <span class="s1">'[Auth] Login Success'</span><span class="p">,</span>
<span class="nx">LOGIN_FAILURE</span> <span class="o">=</span> <span class="s1">'[Auth] Login Failure'</span><span class="p">,</span>
<span class="nx">SIGNUP</span> <span class="o">=</span> <span class="s1">'[Auth] Signup'</span><span class="p">,</span>
<span class="nx">SIGNUP_SUCCESS</span> <span class="o">=</span> <span class="s1">'[Auth] Signup Success'</span><span class="p">,</span>
<span class="nx">SIGNUP_FAILURE</span> <span class="o">=</span> <span class="s1">'[Auth] Signup Failure'</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogIn</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInSuccess</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_SUCCESS</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInFailure</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_FAILURE</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUp</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUpSuccess</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_SUCCESS</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUpFailure</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_FAILURE</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="nx">type</span> <span class="nx">All</span> <span class="o">=</span>
<span class="o">|</span> <span class="nx">LogIn</span>
<span class="o">|</span> <span class="nx">LogInSuccess</span>
<span class="o">|</span> <span class="nx">LogInFailure</span>
<span class="o">|</span> <span class="nx">SignUp</span>
<span class="o">|</span> <span class="nx">SignUpSuccess</span>
<span class="o">|</span> <span class="nx">SignUpFailure</span><span class="p">;</span>
</code></pre></div></div>
<h4 id="add-reducer-to-create-new-state-3">Add reducer (to create new state)</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_FAILURE</span><span class="p">:</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
<span class="p">...</span><span class="nx">state</span><span class="p">,</span>
<span class="na">errorMessage</span><span class="p">:</span> <span class="s1">'That email is already in use.'</span>
<span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>
<h4 id="add-effect-1">Add effect</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Effect</span><span class="p">({</span> <span class="na">dispatch</span><span class="p">:</span> <span class="kc">false</span> <span class="p">})</span>
<span class="nx">SignUpFailure</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">actions</span><span class="p">.</span><span class="nx">pipe</span><span class="p">(</span>
<span class="nx">ofType</span><span class="p">(</span><span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_FAILURE</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>
<blockquote>
<p>You could combine <code class="highlighter-rouge">SignUpFailure</code> and <code class="highlighter-rouge">LogInFailure</code>, to make a single effect:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nx">AuthFailure</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">actions</span><span class="p">.</span><span class="nx">pipe</span><span class="p">(</span>
<span class="nx">ofType</span><span class="p">(</span><span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_FAILURE</span><span class="p">,</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_FAILURE</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div> </div>
</blockquote>
<p>We’re now ready to test!</p>
<p>With the fake back-end running, try signing up with the following credentials:</p>
<ul>
<li><em>email</em>: <code class="highlighter-rouge">test@test.com</code></li>
<li><em>password</em>: <code class="highlighter-rouge">test</code></li>
</ul>
<p>Make sure that you are redirected after a successful attempt and that a token was added to localStorage. Also, ensure that nothing happens on an unsuccessful attempt (when you use an email other than <code class="highlighter-rouge">test@test.com</code>). We still need to wire up the handling of the error to the template.</p>
<h2 id="configure-logout-1">Configure Logout</h2>
<h3 id="logout">Logout</h3>
<h4 id="dispatch-logout-action">Dispatch <code class="highlighter-rouge">LogOut</code> action</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">OnInit</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Store</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppState</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/app.states'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LogOut</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/actions/auth.actions'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="na">selector</span><span class="p">:</span> <span class="s1">'app-landing'</span><span class="p">,</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'./landing.component.html'</span><span class="p">,</span>
<span class="na">styleUrls</span><span class="p">:</span> <span class="p">[</span><span class="s1">'./landing.component.css'</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LandingComponent</span> <span class="kr">implements</span> <span class="nx">OnInit</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span>
<span class="kr">private</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">Store</span><span class="o"><</span><span class="nx">AppState</span><span class="o">></span>
<span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
<span class="nx">ngOnInit</span><span class="p">()</span> <span class="p">{</span>
<span class="p">}</span>
<span class="nx">logOut</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">(</span><span class="k">new</span> <span class="nx">LogOut</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h4 id="add-action-6">Add action</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">enum</span> <span class="nx">AuthActionTypes</span> <span class="p">{</span>
<span class="nx">LOGIN</span> <span class="o">=</span> <span class="s1">'[Auth] Login'</span><span class="p">,</span>
<span class="nx">LOGIN_SUCCESS</span> <span class="o">=</span> <span class="s1">'[Auth] Login Success'</span><span class="p">,</span>
<span class="nx">LOGIN_FAILURE</span> <span class="o">=</span> <span class="s1">'[Auth] Login Failure'</span><span class="p">,</span>
<span class="nx">SIGNUP</span> <span class="o">=</span> <span class="s1">'[Auth] Signup'</span><span class="p">,</span>
<span class="nx">SIGNUP_SUCCESS</span> <span class="o">=</span> <span class="s1">'[Auth] Signup Success'</span><span class="p">,</span>
<span class="nx">SIGNUP_FAILURE</span> <span class="o">=</span> <span class="s1">'[Auth] Signup Failure'</span><span class="p">,</span>
<span class="nx">LOGOUT</span> <span class="o">=</span> <span class="s1">'[Auth] Logout'</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogIn</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInSuccess</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_SUCCESS</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInFailure</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_FAILURE</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUp</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUpSuccess</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_SUCCESS</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUpFailure</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_FAILURE</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogOut</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGOUT</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">export</span> <span class="nx">type</span> <span class="nx">All</span> <span class="o">=</span>
<span class="o">|</span> <span class="nx">LogIn</span>
<span class="o">|</span> <span class="nx">LogInSuccess</span>
<span class="o">|</span> <span class="nx">LogInFailure</span>
<span class="o">|</span> <span class="nx">SignUp</span>
<span class="o">|</span> <span class="nx">SignUpSuccess</span>
<span class="o">|</span> <span class="nx">SignUpFailure</span>
<span class="o">|</span> <span class="nx">LogOut</span><span class="p">;</span>
</code></pre></div></div>
<p>Did you notice we’re not sending any parameters with the dispatch? Because of that, we left off the payload in the constructor.</p>
<h4 id="add-reducer-to-create-new-state-4">Add reducer (to create new state)</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGOUT</span><span class="p">:</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">initialState</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h4 id="add-effect-to-remove-token-from-localstorage">Add effect (to remove token from localStorage)</h4>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Effect</span><span class="p">({</span> <span class="na">dispatch</span><span class="p">:</span> <span class="kc">false</span> <span class="p">})</span>
<span class="kr">public</span> <span class="nx">LogOut</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">actions</span><span class="p">.</span><span class="nx">pipe</span><span class="p">(</span>
<span class="nx">ofType</span><span class="p">(</span><span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGOUT</span><span class="p">),</span>
<span class="nx">tap</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">localStorage</span><span class="p">.</span><span class="nx">removeItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">);</span>
</code></pre></div></div>
<p>Import:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span>
<span class="nx">AuthActionTypes</span><span class="p">,</span>
<span class="nx">LogIn</span><span class="p">,</span> <span class="nx">LogInSuccess</span><span class="p">,</span> <span class="nx">LogInFailure</span><span class="p">,</span>
<span class="nx">SignUp</span><span class="p">,</span> <span class="nx">SignUpSuccess</span><span class="p">,</span> <span class="nx">SignUpFailure</span><span class="p">,</span>
<span class="nx">LogOut</span><span class="p">,</span>
<span class="p">}</span> <span class="k">from</span> <span class="s1">'../actions/auth.actions'</span><span class="p">;</span>
</code></pre></div></div>
<p>We’ll test this functionality out shortly.</p>
<h2 id="update-the-templates-1">Update the Templates</h2>
<h3 id="add-error-messages-to-the-forms">Add error messages to the forms</h3>
<h4 id="update-the-components">Update the components</h4>
<p><code class="highlighter-rouge">LogInComponent</code>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">OnInit</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Store</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Observable</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'rxjs/Observable'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../models/user'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppState</span><span class="p">,</span> <span class="nx">selectAuthState</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/app.states'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LogIn</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/actions/auth.actions'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="na">selector</span><span class="p">:</span> <span class="s1">'app-log-in'</span><span class="p">,</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'./log-in.component.html'</span><span class="p">,</span>
<span class="na">styleUrls</span><span class="p">:</span> <span class="p">[</span><span class="s1">'./log-in.component.css'</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInComponent</span> <span class="kr">implements</span> <span class="nx">OnInit</span> <span class="p">{</span>
<span class="nl">user</span><span class="p">:</span> <span class="nx">User</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">User</span><span class="p">();</span>
<span class="nl">getState</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span><span class="p">;</span>
<span class="nl">errorMessage</span><span class="p">:</span> <span class="nx">string</span> <span class="o">|</span> <span class="kc">null</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span>
<span class="kr">private</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">Store</span><span class="o"><</span><span class="nx">AppState</span><span class="o">></span>
<span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">getState</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">select</span><span class="p">(</span><span class="nx">selectAuthState</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">ngOnInit</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">getState</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">errorMessage</span> <span class="o">=</span> <span class="nx">state</span><span class="p">.</span><span class="nx">errorMessage</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="nx">onSubmit</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">email</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">password</span>
<span class="p">};</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">(</span><span class="k">new</span> <span class="nx">LogIn</span><span class="p">(</span><span class="nx">payload</span><span class="p">));</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here, we’re subscribing to the store and assigning the <code class="highlighter-rouge">errorMessage</code> to <code class="highlighter-rouge">this.errorMessage</code>, which we can reference in our template.</p>
<p>Update <em>app.states.ts</em>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">createFeatureSelector</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">auth</span> <span class="k">from</span> <span class="s1">'./reducers/auth.reducers'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">AppState</span> <span class="p">{</span>
<span class="nl">authState</span><span class="p">:</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">State</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">reducers</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">auth</span><span class="p">:</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">reducer</span>
<span class="p">};</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">selectAuthState</span> <span class="o">=</span> <span class="nx">createFeatureSelector</span><span class="o"><</span><span class="nx">AppState</span><span class="o">></span><span class="p">(</span><span class="s1">'auth'</span><span class="p">);</span>
</code></pre></div></div>
<p><code class="highlighter-rouge">createFeatureSelector</code> is a <a href="https://ngrx.io/guide/store/selectors">selector</a> used to query the state.</p>
<p><code class="highlighter-rouge">SignUpComponent</code>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">OnInit</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Store</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Observable</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'rxjs/Observable'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../models/user'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppState</span><span class="p">,</span> <span class="nx">selectAuthState</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/app.states'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">SignUp</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/actions/auth.actions'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="na">selector</span><span class="p">:</span> <span class="s1">'app-sign-up'</span><span class="p">,</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'./sign-up.component.html'</span><span class="p">,</span>
<span class="na">styleUrls</span><span class="p">:</span> <span class="p">[</span><span class="s1">'./sign-up.component.css'</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUpComponent</span> <span class="kr">implements</span> <span class="nx">OnInit</span> <span class="p">{</span>
<span class="nl">user</span><span class="p">:</span> <span class="nx">User</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">User</span><span class="p">();</span>
<span class="nl">getState</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span><span class="p">;</span>
<span class="nl">errorMessage</span><span class="p">:</span> <span class="nx">string</span> <span class="o">|</span> <span class="kc">null</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span>
<span class="kr">private</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">Store</span><span class="o"><</span><span class="nx">AppState</span><span class="o">></span>
<span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">getState</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">select</span><span class="p">(</span><span class="nx">selectAuthState</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">ngOnInit</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">getState</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">errorMessage</span> <span class="o">=</span> <span class="nx">state</span><span class="p">.</span><span class="nx">errorMessage</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="nx">onSubmit</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">email</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">password</span>
<span class="p">};</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">(</span><span class="k">new</span> <span class="nx">SignUp</span><span class="p">(</span><span class="nx">payload</span><span class="p">));</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h4 id="add-messages-to-the-templates">Add messages to the templates</h4>
<p>Login:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><div class="row">
<div class="col-md-4">
<h1>Log in</h1>
<hr><br>
<div *ngIf="errorMessage">
<div class="alert alert-danger" role="alert">
{{errorMessage}}
</div>
</div>
<form (ngSubmit)="onSubmit()" ngNativeValidate>
<div class="form-group">
<label for="email">Email</label>
<input
[(ngModel)]="user.email"
name="email"
type="email"
required
class="form-control"
id="email"
placeholder="enter your email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input
[(ngModel)]="user.password"
name="password"
type="password"
required
class="form-control"
id="password"
placeholder="enter a password">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
<a [routerLink]="['/']" class="btn btn-success">Cancel</a>
</form>
<p>
<span>Don't have an account?&nbsp;</span>
<a [routerLink]="['/sign-up']">Sign up!</a>
</p>
</div>
</div>
</code></pre></div></div>
<p>Signup:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><div class="row">
<div class="col-md-4">
<h1>Sign up</h1>
<hr><br>
<div *ngIf="errorMessage">
<div class="alert alert-danger" role="alert">
{{errorMessage}}
</div>
</div>
<form (ngSubmit)="onSubmit()" ngNativeValidate>
<div class="form-group">
<label for="email">Email</label>
<input
[(ngModel)]="user.email"
name="email"
type="email"
required
class="form-control"
id="email"
placeholder="enter your email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input
[(ngModel)]="user.password"
name="password"
type="password"
required
class="form-control"
id="password"
placeholder="enter a password">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
<a [routerLink]="['/']" class="btn btn-success">Cancel</a>
</form>
<p>
<span>Already have an account?&nbsp;</span>
<a [routerLink]="['/log-in']">Log in!</a>
</p>
</div>
</div>
</code></pre></div></div>
<p>Test this out!</p>
<div style="text-align:left;padding-top:10px;padding-left:10px;padding-bottom:20px">
<img src="/assets/img/blog/angular-auth-ngrx/angular-ngrx-error-messages.gif" style="max-width: 70%;border:0;box-shadow:none;" alt="error messages" />
</div>
<h3 id="update-the-landingcomponent">Update the <code class="highlighter-rouge">LandingComponent</code></h3>
<p>Update the component:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">OnInit</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Store</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Observable</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'rxjs/Observable'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppState</span><span class="p">,</span> <span class="nx">selectAuthState</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/app.states'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LogOut</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/actions/auth.actions'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="na">selector</span><span class="p">:</span> <span class="s1">'app-landing'</span><span class="p">,</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'./landing.component.html'</span><span class="p">,</span>
<span class="na">styleUrls</span><span class="p">:</span> <span class="p">[</span><span class="s1">'./landing.component.css'</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LandingComponent</span> <span class="kr">implements</span> <span class="nx">OnInit</span> <span class="p">{</span>
<span class="nl">getState</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span><span class="p">;</span>
<span class="nl">isAuthenticated</span><span class="p">:</span> <span class="kc">false</span><span class="p">;</span>
<span class="nx">user</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="nx">errorMessage</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span>
<span class="kr">private</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">Store</span><span class="o"><</span><span class="nx">AppState</span><span class="o">></span>
<span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">getState</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">select</span><span class="p">(</span><span class="nx">selectAuthState</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">ngOnInit</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">getState</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">isAuthenticated</span> <span class="o">=</span> <span class="nx">state</span><span class="p">.</span><span class="nx">isAuthenticated</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">user</span> <span class="o">=</span> <span class="nx">state</span><span class="p">.</span><span class="nx">user</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">errorMessage</span> <span class="o">=</span> <span class="nx">state</span><span class="p">.</span><span class="nx">errorMessage</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="nx">logOut</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">(</span><span class="k">new</span> <span class="nx">LogOut</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Next, update the Landing template so that the <em>Log in</em> and <em>Sign up</em> buttons are <em>only</em> visible when a user is not authenticated. Also, when the user is authenticated, a welcome message will be displayed along with a <em>Log out</em> button.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><div class="row">
<div class="col-md-4">
<h1>Angular + NGRX</h1>
<hr><br>
<div *ngIf="isAuthenticated; then doSomething; else doSomethingElse;"></div>
<ng-template #doSomething>
<p>You logged in <em>{{user.email}}!</em></p>
<button class="btn btn-primary" (click)="logOut()">Log out</button>
</ng-template>
<ng-template #doSomethingElse>
<a [routerLink]="['/log-in']" class="btn btn-primary">Log in</a>
<a [routerLink]="['/sign-up']" class="btn btn-primary">Sign up</a>
</ng-template>
</div>
</div>
</code></pre></div></div>
<p>Test it out! Make sure the token is removed when the user logs out.</p>
<div style="text-align:left;padding-top:10px;padding-left:10px;padding-bottom:20px;">
<img src="/assets/img/blog/angular-auth-ngrx/landing-logged-in.png" style="max-width: 70%;border:0;box-shadow:none;" alt="landing component" />
</div>
<div style="text-align:left;padding-top:10px;padding-left:10px;padding-bottom:20px;">
<img src="/assets/img/blog/angular-auth-ngrx/landing-logged-out.png" style="max-width: 70%;border:0;box-shadow:none;" alt="landing component" />
</div>
<p>Finally, update the template once again to show the current application state. This is just for reference while developing, so be sure to remove it before deploying to production.</p>
<p>Template:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><div class="row">
<div class="col-md-4">
<h1>Angular + NGRX</h1>
<hr><br>
<div *ngIf="isAuthenticated; then doSomething; else doSomethingElse;"></div>
<ng-template #doSomething>
<p>You logged in <em>{{user.email}}!</em></p>
<button class="btn btn-primary" (click)="logOut()">Log out</button>
</ng-template>
<ng-template #doSomethingElse>
<a [routerLink]="['/log-in']" class="btn btn-primary">Log in</a>
<a [routerLink]="['/sign-up']" class="btn btn-primary">Sign up</a>
</ng-template>
<br><br><br>
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">Current State</h5>
<ul>
<li><strong>isAuthenticated</strong> - {{isAuthenticated}}</li>
<li><strong>user.email</strong> - {{ user?.email || 'null'}}</li>
<li><strong>user.token</strong> - {{ user?.token || 'null'}}</li>
<li><strong>errorMessage</strong> - {{ errorMessage || 'null'}}</li>
</ul>
</div>
</div>
</div>
</div>
</code></pre></div></div>
<div style="text-align:left;padding-top:10px;padding-left:10px;padding-bottom:20px">
<img src="/assets/img/blog/angular-auth-ngrx/angular-ngrx-landing-show-state.gif" style="max-width: 70%;border:0;box-shadow:none;" alt="state on landing page" />
</div>
<h2 id="add-http-interceptor-1">Add HTTP Interceptor</h2>
<h3 id="configure-the-interceptor">Configure the interceptor</h3>
<p>The <code class="highlighter-rouge">HttpInterceptor</code> <a href="https://angular.io/api/common/http/HttpInterceptor">interface</a> is used to intercept and modify HTTP requests globally. We’ll use it to add the authentication token and content type to the request headers.</p>
<p>Create the service:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ng generate service services/token
</code></pre></div></div>
<p>Rename <em>token.service.ts</em> to <em>token.interceptor.ts</em> and then remove <em>token.service.spec.ts</em>.</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Injectable</span><span class="p">,</span> <span class="nx">Injector</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span>
<span class="nx">HttpEvent</span><span class="p">,</span> <span class="nx">HttpInterceptor</span><span class="p">,</span> <span class="nx">HttpHandler</span><span class="p">,</span> <span class="nx">HttpRequest</span>
<span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/common/http'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Observable</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'rxjs/Observable'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AuthService</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./auth.service'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">TokenInterceptor</span> <span class="kr">implements</span> <span class="nx">HttpInterceptor</span> <span class="p">{</span>
<span class="kr">private</span> <span class="nx">authService</span><span class="p">:</span> <span class="nx">AuthService</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">private</span> <span class="nx">injector</span><span class="p">:</span> <span class="nx">Injector</span><span class="p">)</span> <span class="p">{}</span>
<span class="nx">intercept</span><span class="p">(</span><span class="nx">request</span><span class="p">:</span> <span class="nx">HttpRequest</span><span class="o"><</span><span class="nx">any</span><span class="o">></span><span class="p">,</span> <span class="nx">next</span><span class="p">:</span> <span class="nx">HttpHandler</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">HttpEvent</span><span class="o"><</span><span class="nx">any</span><span class="o">>></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authService</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">injector</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">AuthService</span><span class="p">);</span>
<span class="kd">const</span> <span class="na">token</span><span class="p">:</span> <span class="nx">string</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">authService</span><span class="p">.</span><span class="nx">getToken</span><span class="p">();</span>
<span class="nx">request</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">clone</span><span class="p">({</span>
<span class="na">setHeaders</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'Authorization'</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">token</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="s1">'Content-Type'</span><span class="p">:</span> <span class="s1">'application/json'</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="k">return</span> <span class="nx">next</span><span class="p">.</span><span class="nx">handle</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Import the <code class="highlighter-rouge">TokenInterceptor</code> and <code class="highlighter-rouge">HTTP_INTERCEPTORS</code> from <code class="highlighter-rouge">@angular/common/http</code> to <em>src/app/app.module.ts</em>, and then add the interceptor as a provider in the <code class="highlighter-rouge">@NgModule</code> definition:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">BrowserModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/platform-browser'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">NgModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">RouterModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/router'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">FormsModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/forms'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">HttpClientModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/common/http'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">EffectsModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/effects'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">StoreModule</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">HTTP_INTERCEPTORS</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/common/http'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./app.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LandingComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/landing/landing.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">SignUpComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/sign-up/sign-up.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">LogInComponent</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./components/log-in/log-in.component'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AuthService</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./services/auth.service'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AuthEffects</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./store/effects/auth.effects'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">reducers</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./store/app.states'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">TokenInterceptor</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./services/token.interceptor'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">NgModule</span><span class="p">({</span>
<span class="na">declarations</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">AppComponent</span><span class="p">,</span>
<span class="nx">LandingComponent</span><span class="p">,</span>
<span class="nx">SignUpComponent</span><span class="p">,</span>
<span class="nx">LogInComponent</span>
<span class="p">],</span>
<span class="na">imports</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">BrowserModule</span><span class="p">,</span>
<span class="nx">FormsModule</span><span class="p">,</span>
<span class="nx">HttpClientModule</span><span class="p">,</span>
<span class="nx">StoreModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">(</span><span class="nx">reducers</span><span class="p">,</span> <span class="p">{}),</span>
<span class="nx">EffectsModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">([</span><span class="nx">AuthEffects</span><span class="p">]),</span>
<span class="nx">RouterModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">([</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'log-in'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LogInComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'sign-up'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">SignUpComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LandingComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'**'</span><span class="p">,</span> <span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span> <span class="p">}</span>
<span class="p">])</span>
<span class="p">],</span>
<span class="na">providers</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">AuthService</span><span class="p">,</span>
<span class="p">{</span>
<span class="na">provide</span><span class="p">:</span> <span class="nx">HTTP_INTERCEPTORS</span><span class="p">,</span>
<span class="na">useClass</span><span class="p">:</span> <span class="nx">TokenInterceptor</span><span class="p">,</span>
<span class="na">multi</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">},</span>
<span class="p">],</span>
<span class="na">bootstrap</span><span class="p">:</span> <span class="p">[</span><span class="nx">AppComponent</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">AppModule</span> <span class="p">{</span> <span class="p">}</span>
</code></pre></div></div>
<p>Now, when an HTTP request is made, the token (if it exists in localStorage) will be added to the header.</p>
<h3 id="handle-unauthorized-responses">Handle unauthorized responses</h3>
<p>The interceptor can also be used to intercept incoming HTTP responses. We can use it here to check for any <code class="highlighter-rouge">401</code> codes and redirect the user to the log in route:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Injectable</span><span class="p">,</span> <span class="nx">Injector</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span>
<span class="nx">HttpEvent</span><span class="p">,</span> <span class="nx">HttpInterceptor</span><span class="p">,</span> <span class="nx">HttpHandler</span><span class="p">,</span> <span class="nx">HttpRequest</span><span class="p">,</span>
<span class="nx">HttpResponse</span><span class="p">,</span> <span class="nx">HttpErrorResponse</span>
<span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/common/http'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Observable</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'rxjs/Observable'</span><span class="p">;</span>
<span class="k">import</span> <span class="s1">'rxjs/add/operator/do'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Router</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/router'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AuthService</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./auth.service'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">TokenInterceptor</span> <span class="kr">implements</span> <span class="nx">HttpInterceptor</span> <span class="p">{</span>
<span class="kr">private</span> <span class="nx">authService</span><span class="p">:</span> <span class="nx">AuthService</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">private</span> <span class="nx">injector</span><span class="p">:</span> <span class="nx">Injector</span><span class="p">)</span> <span class="p">{}</span>
<span class="nx">intercept</span><span class="p">(</span><span class="nx">request</span><span class="p">:</span> <span class="nx">HttpRequest</span><span class="o"><</span><span class="nx">any</span><span class="o">></span><span class="p">,</span> <span class="nx">next</span><span class="p">:</span> <span class="nx">HttpHandler</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">HttpEvent</span><span class="o"><</span><span class="nx">any</span><span class="o">>></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authService</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">injector</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">AuthService</span><span class="p">);</span>
<span class="kd">const</span> <span class="na">token</span><span class="p">:</span> <span class="nx">string</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">authService</span><span class="p">.</span><span class="nx">getToken</span><span class="p">();</span>
<span class="nx">request</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">clone</span><span class="p">({</span>
<span class="na">setHeaders</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'Authorization'</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">token</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="s1">'Content-Type'</span><span class="p">:</span> <span class="s1">'application/json'</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="k">return</span> <span class="nx">next</span><span class="p">.</span><span class="nx">handle</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">ErrorInterceptor</span> <span class="kr">implements</span> <span class="nx">HttpInterceptor</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">private</span> <span class="na">router</span><span class="p">:</span> <span class="nx">Router</span><span class="p">)</span> <span class="p">{}</span>
<span class="nx">intercept</span><span class="p">(</span><span class="na">request</span><span class="p">:</span> <span class="nx">HttpRequest</span><span class="o"><</span><span class="nx">any</span><span class="o">></span><span class="p">,</span> <span class="na">next</span><span class="p">:</span> <span class="nx">HttpHandler</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">HttpEvent</span><span class="o"><</span><span class="nx">any</span><span class="o">>></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">next</span><span class="p">.</span><span class="nx">handle</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="na">response</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">response</span> <span class="k">instanceof</span> <span class="nx">HttpErrorResponse</span> <span class="o">&&</span> <span class="nx">response</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">401</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">response</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">Observable</span><span class="p">.</span><span class="k">throw</span><span class="p">(</span><span class="nx">response</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Add the <code class="highlighter-rouge">ErrorInterceptor</code> to the provider array:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">providers</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">AuthService</span><span class="p">,</span>
<span class="p">{</span>
<span class="na">provide</span><span class="p">:</span> <span class="nx">HTTP_INTERCEPTORS</span><span class="p">,</span>
<span class="na">useClass</span><span class="p">:</span> <span class="nx">TokenInterceptor</span><span class="p">,</span>
<span class="na">multi</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">provide</span><span class="p">:</span> <span class="nx">HTTP_INTERCEPTORS</span><span class="p">,</span>
<span class="na">useClass</span><span class="p">:</span> <span class="nx">ErrorInterceptor</span><span class="p">,</span>
<span class="na">multi</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="p">],</span>
</code></pre></div></div>
<p>Make sure to import it:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span>
<span class="nx">TokenInterceptor</span><span class="p">,</span> <span class="nx">ErrorInterceptor</span>
<span class="p">}</span> <span class="k">from</span> <span class="s1">'./services/token.interceptor'</span><span class="p">;</span>
</code></pre></div></div>
<p>We’ll test this out shortly.</p>
<h3 id="add-new-route">Add new route</h3>
<p>Generate the component:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ng generate component components/status
</code></pre></div></div>
<p>Update the router:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">RouterModule</span><span class="p">.</span><span class="nx">forRoot</span><span class="p">([</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'log-in'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LogInComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'sign-up'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">SignUpComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'status'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">StatusComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">LandingComponent</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">path</span><span class="p">:</span> <span class="s1">'**'</span><span class="p">,</span> <span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span> <span class="p">}</span>
<span class="p">])</span>
</code></pre></div></div>
<p>Dispatch a new action in the component:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">OnInit</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Store</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AppState</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/app.states'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">GetStatus</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'../../store/actions/auth.actions'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="na">selector</span><span class="p">:</span> <span class="s1">'app-status'</span><span class="p">,</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'./status.component.html'</span><span class="p">,</span>
<span class="na">styleUrls</span><span class="p">:</span> <span class="p">[</span><span class="s1">'./status.component.css'</span><span class="p">]</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">StatusComponent</span> <span class="kr">implements</span> <span class="nx">OnInit</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">private</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">Store</span><span class="o"><</span><span class="nx">AppState</span><span class="o">></span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
<span class="nx">ngOnInit</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">(</span><span class="k">new</span> <span class="nx">GetStatus</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Add the action:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@ngrx/store'</span><span class="p">;</span>
<span class="k">export</span> <span class="kr">enum</span> <span class="nx">AuthActionTypes</span> <span class="p">{</span>
<span class="nx">LOGIN</span> <span class="o">=</span> <span class="s1">'[Auth] Login'</span><span class="p">,</span>
<span class="nx">LOGIN_SUCCESS</span> <span class="o">=</span> <span class="s1">'[Auth] Login Success'</span><span class="p">,</span>
<span class="nx">LOGIN_FAILURE</span> <span class="o">=</span> <span class="s1">'[Auth] Login Failure'</span><span class="p">,</span>
<span class="nx">SIGNUP</span> <span class="o">=</span> <span class="s1">'[Auth] Signup'</span><span class="p">,</span>
<span class="nx">SIGNUP_SUCCESS</span> <span class="o">=</span> <span class="s1">'[Auth] Signup Success'</span><span class="p">,</span>
<span class="nx">SIGNUP_FAILURE</span> <span class="o">=</span> <span class="s1">'[Auth] Signup Failure'</span><span class="p">,</span>
<span class="nx">LOGOUT</span> <span class="o">=</span> <span class="s1">'[Auth] Logout'</span><span class="p">,</span>
<span class="nx">GET_STATUS</span> <span class="o">=</span> <span class="s1">'[Auth] GetStatus'</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogIn</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInSuccess</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_SUCCESS</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogInFailure</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGIN_FAILURE</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUp</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUpSuccess</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_SUCCESS</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">SignUpFailure</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">SIGNUP_FAILURE</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">public</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">LogOut</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">LOGOUT</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">GetStatus</span> <span class="kr">implements</span> <span class="nx">Action</span> <span class="p">{</span>
<span class="nx">readonly</span> <span class="nx">type</span> <span class="o">=</span> <span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">GET_STATUS</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">export</span> <span class="nx">type</span> <span class="nx">All</span> <span class="o">=</span>
<span class="o">|</span> <span class="nx">LogIn</span>
<span class="o">|</span> <span class="nx">LogInSuccess</span>
<span class="o">|</span> <span class="nx">LogInFailure</span>
<span class="o">|</span> <span class="nx">SignUp</span>
<span class="o">|</span> <span class="nx">SignUpSuccess</span>
<span class="o">|</span> <span class="nx">SignUpFailure</span>
<span class="o">|</span> <span class="nx">LogOut</span>
<span class="o">|</span> <span class="nx">GetStatus</span><span class="p">;</span>
</code></pre></div></div>
<p>Effect:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Effect</span><span class="p">({</span> <span class="na">dispatch</span><span class="p">:</span> <span class="kc">false</span> <span class="p">})</span>
<span class="nx">GetStatus</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">actions</span>
<span class="p">.</span><span class="nx">ofType</span><span class="p">(</span><span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">GET_STATUS</span><span class="p">)</span>
<span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">action</span><span class="p">:</span> <span class="nx">GetStatus</span><span class="p">)</span> <span class="o">=></span> <span class="nx">action</span><span class="p">)</span>
<span class="p">.</span><span class="nx">switchMap</span><span class="p">(</span><span class="nx">payload</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">authService</span><span class="p">.</span><span class="nx">getStatus</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">@</span><span class="nd">Effect</span><span class="p">({</span> <span class="na">dispatch</span><span class="p">:</span> <span class="kc">false</span> <span class="p">})</span>
<span class="na">GetStatus</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">any</span><span class="o">></span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">actions</span>
<span class="p">.</span><span class="nx">ofType</span><span class="p">(</span><span class="nx">AuthActionTypes</span><span class="p">.</span><span class="nx">GET_STATUS</span><span class="p">)</span>
<span class="p">.</span><span class="nx">switchMap</span><span class="p">(</span><span class="nx">payload</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">authService</span><span class="p">.</span><span class="nx">getStatus</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Add the <code class="highlighter-rouge">getStatus</code> method to <code class="highlighter-rouge">AuthService</code>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">getStatus</span><span class="p">():</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">User</span><span class="o">></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">BASE_URL</span><span class="p">}</span><span class="s2">/status`</span><span class="p">;</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">http</span><span class="p">.</span><span class="kd">get</span><span class="o"><</span><span class="nx">User</span><span class="o">></span><span class="p">(</span><span class="nx">url</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Remove the token from localStorage (if it exists), since it’s invalid, and redirect the user:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">ErrorInterceptor</span> <span class="kr">implements</span> <span class="nx">HttpInterceptor</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="kr">private</span> <span class="nx">router</span><span class="p">:</span> <span class="nx">Router</span><span class="p">)</span> <span class="p">{}</span>
<span class="nx">intercept</span><span class="p">(</span><span class="nx">request</span><span class="p">:</span> <span class="nx">HttpRequest</span><span class="o"><</span><span class="nx">any</span><span class="o">></span><span class="p">,</span> <span class="nx">next</span><span class="p">:</span> <span class="nx">HttpHandler</span><span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">HttpEvent</span><span class="o"><</span><span class="nx">any</span><span class="o">>></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">next</span><span class="p">.</span><span class="nx">handle</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="na">response</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">response</span> <span class="k">instanceof</span> <span class="nx">HttpErrorResponse</span> <span class="o">&&</span> <span class="nx">response</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">401</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">localStorage</span><span class="p">.</span><span class="nx">removeItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">router</span><span class="p">.</span><span class="nx">navigateByUrl</span><span class="p">(</span><span class="s1">'/log-in'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">Observable</span><span class="p">.</span><span class="k">throw</span><span class="p">(</span><span class="nx">response</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Add a <code class="highlighter-rouge">/status</code> link to the Landing template:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><div class="row">
<div class="col-md-4">
<h1>Angular + NGRX</h1>
<hr><br>
<div *ngIf="isAuthenticated; then doSomething; else doSomethingElse;"></div>
<ng-template #doSomething>
<p>You logged in <em>{{user.email}}!</em></p>
<button class="btn btn-primary" (click)="logOut()">Log out</button>
</ng-template>
<ng-template #doSomethingElse>
<a [routerLink]="['/log-in']" class="btn btn-primary">Log in</a>
<a [routerLink]="['/sign-up']" class="btn btn-primary">Sign up</a>
</ng-template>
<a [routerLink]="['/status']" class="btn btn-primary">Status</a>
<br><br><br>
<div class="card" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">Current State</h5>
<ul>
<li><strong>isAuthenticated</strong> - {{isAuthenticated}}</li>
<li><strong>user.email</strong> - {{ user?.email || 'null'}}</li>
<li><strong>user.token</strong> - {{ user?.token || 'null'}}</li>
<li><strong>errorMessage</strong> - {{ errorMessage || 'null'}}</li>
</ul>
</div>
</div>
</div>
</div>
</code></pre></div></div>
<p>Status template:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><div class="row">
<div class="col-md-4">
<h1>Status Works!</h1>
<hr><br>
<a [routerLink]="['/']" class="btn btn-primary">Home</a>
</div>
</code></pre></div></div>
<p>Now, if you try to access <code class="highlighter-rouge">/status</code> and are <em>not</em> logged in, you will be redirected to the <code class="highlighter-rouge">/log-in</code> route:</p>
<div style="text-align:left;padding-top:10px;padding-left:10px;">
<img src="/assets/img/blog/angular-auth-ngrx/angular-ngrx-http-interceptor.gif" style="max-width: 70%;border:0;box-shadow:none;" alt="http interceptor" />
</div>
<p>You should also see the token added to the header when you hit the <code class="highlighter-rouge">/status</code> route:</p>
<div style="text-align:left;padding-top:10px;padding-left:10px;padding-bottom:20px;">
<img src="/assets/img/blog/angular-auth-ngrx/status.png" style="max-width: 70%;border:0;box-shadow:none;" alt="status component" />
</div>
<h2 id="route-guard-1">Route Guard</h2>
<p>Try going to <a href="http://localhost:4200/status">http://localhost:4200/status</a> in the browser. Did you notice that the component will render for a second before re-directing? That’s because <code class="highlighter-rouge">ngOnInit()</code> is fired <em>after</em> the component is created. We can use a route guard to prevent access to the route altogether so the redirect will happen <em>before</em> the component gets created.</p>
<h3 id="configure-the-interface">Configure the interface</h3>
<p>Create the service:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ng generate service services/auth-guard
</code></pre></div></div>
<p>Update:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Injectable</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/core'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Router</span><span class="p">,</span> <span class="nx">CanActivate</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/router'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">AuthService</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./auth.service'</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">AuthGuardService</span> <span class="kr">implements</span> <span class="nx">CanActivate</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span>
<span class="kr">public</span> <span class="nx">auth</span><span class="p">:</span> <span class="nx">AuthService</span><span class="p">,</span>
<span class="kr">public</span> <span class="nx">router</span><span class="p">:</span> <span class="nx">Router</span>
<span class="p">)</span> <span class="p">{}</span>
<span class="nx">canActivate</span><span class="p">():</span> <span class="kr">boolean</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">getToken</span><span class="p">())</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">router</span><span class="p">.</span><span class="nx">navigateByUrl</span><span class="p">(</span><span class="s1">'/log-in'</span><span class="p">);</span>
<span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>So, we are using the <code class="highlighter-rouge">CanActivate</code> route guard <a href="https://angular.io/api/router/CanActivate">interface</a> to implement the guard itself. In the <code class="highlighter-rouge">canActivate</code> method, we check to see if a token is in localStorage, return the appropriate boolean, and (if necessary) redirect the user.</p>
<blockquote>
<p>It’s probably worth looking at state as well, to get the value of <code class="highlighter-rouge">isAuthenticated</code>. Make this change on your own.</p>
</blockquote>
<h3 id="protect-the-route">Protect the route</h3>
<p>Update the route in <em>app.module.ts</em>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nl">path</span><span class="p">:</span> <span class="s1">'status'</span><span class="p">,</span> <span class="nx">component</span><span class="p">:</span> <span class="nx">StatusComponent</span><span class="p">,</span> <span class="nx">canActivate</span><span class="p">:</span> <span class="p">[</span><span class="nx">AuthGuard</span><span class="p">]</span> <span class="p">},</span>
</code></pre></div></div>
<p>Add the provider:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">providers</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">AuthService</span><span class="p">,</span>
<span class="nx">AuthGuard</span><span class="p">,</span>
<span class="p">{</span>
<span class="na">provide</span><span class="p">:</span> <span class="nx">HTTP_INTERCEPTORS</span><span class="p">,</span>
<span class="na">useClass</span><span class="p">:</span> <span class="nx">TokenInterceptor</span><span class="p">,</span>
<span class="na">multi</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">provide</span><span class="p">:</span> <span class="nx">HTTP_INTERCEPTORS</span><span class="p">,</span>
<span class="na">useClass</span><span class="p">:</span> <span class="nx">ErrorInterceptor</span><span class="p">,</span>
<span class="na">multi</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="p">],</span>
</code></pre></div></div>
<p>Import <code class="highlighter-rouge">canActivate</code>:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">RouterModule</span><span class="p">,</span> <span class="nx">CanActivate</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'@angular/router'</span><span class="p">;</span>
</code></pre></div></div>
<p>Import the service:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">AuthGuardService</span> <span class="k">as</span> <span class="nx">AuthGuard</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'./services/auth-guard.service'</span><span class="p">;</span>
</code></pre></div></div>
<p>You should no longer see the template flicker before being redirected.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This article took a look at how to add authentication to an Angular app using NGRX Store (to manage state) and Effects (to manage side-effects). The full code can be found in the <a href="https://github.com/mjhea0/angular-auth-ngrx">angular-auth-ngrx</a> repository.</p>
<blockquote>
<p>Want to learn how to test this app? Check out the <a href="https://testdriven.io/testing-angular-with-cypress-and-docker">Testing Angular with Cypress and Docker</a> blog post!</p>
</blockquote>
<p>Looking for some challenges?</p>
<ol>
<li>Add some additional actions and effects: <code class="highlighter-rouge">[Auth] Signup Redirect</code> and <code class="highlighter-rouge">[Auth] Login Redirect</code></li>
<li>Refactor out native form validation (<code class="highlighter-rouge">ngNativeValidate</code>) and add in reactive Angular form validation</li>
<li>Add unit and end-to-end tests</li>
<li>Configure NGRX <a href="https://ngrx.io/guide/router-store">Router Store</a> so that the Angular Router has access to state</li>
<li>Add Docker to simplify the development workflow (see <a href="http://mherman.org/blog/2018/02/26/dockerizing-an-angular-app">Dockerizing an Angular App</a> for more info)</li>
<li>Remove all <code class="highlighter-rouge">console.log</code> statements</li>
<li>Use a <a href="https://stackoverflow.com/questions/43466159/how-to-set-a-cookie-for-jwt-token-in-angular-2/43475915">cookie instead of localStorage</a></li>
</ol>
Michael Herman
In this tutorial, we’ll add authentication to Angular using NGRX Store and Effects.
Stubbing Node Authentication Middleware with Sinon
2018-01-22T00:00:00-06:00
2018-01-22T00:00:00-06:00
https://mherman.org/blog/stubbing-node-authentication-middleware-with-sinon
<p><a href="http://mherman.org/blog/2018/01/02/user-authentication-with-passport-and-koa/">Last time</a> we looked at how to set up <a href="http://www.passportjs.org/">Passport</a> local authentication with Node and Koa. We took a test-first approach and wrote the majority of tests first. That said, there were a two routes that we could not test (<code class="highlighter-rouge">/auth/status</code> and <code class="highlighter-rouge">/auth/logout</code>) since they required us to to bypass the <code class="highlighter-rouge">isAuthenticated()</code> method and manually set a cookie.</p>
<p>In this post, we’ll look at how to stub Passport authentication middleware and calls to both Postgres and Redis with <a href="http://sinonjs.org/">Sinon.js</a>.</p>
<div style="text-align:center;">
<img src="/assets/img/blog/koa/sinon-passport.png" style="max-width: 90%; border:0; box-shadow: none;" alt="sinon.js and passport.js" />
</div>
<h3 id="parts">Parts</h3>
<p>This article is part of a 4-part Koa and Sinon series…</p>
<ol>
<li><a href="http://mherman.org/blog/2017/08/23/building-a-restful-api-with-koa-and-postgres">Building a RESTful API with Koa and Postgres</a></li>
<li><a href="http://mherman.org/blog/2017/11/06/stubbing-http-requests-with-sinon">Stubbing HTTP Requests with Sinon</a></li>
<li><a href="http://mherman.org/blog/2018/01/02/user-authentication-with-passport-and-koa">User Authentication with Passport and Koa</a></li>
<li><a href="http://mherman.org/blog/2018/01/22/stubbing-node-authentication-middleware-with-sinon">Stubbing Node Authentication Middleware with Sinon</a> (this article)</li>
</ol>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#test-structure" id="markdown-toc-test-structure">Test Structure</a></li>
<li><a href="#why-stub" id="markdown-toc-why-stub">Why Stub?</a></li>
<li><a href="#postgres-stub" id="markdown-toc-postgres-stub">Postgres Stub</a></li>
<li><a href="#passport-stub" id="markdown-toc-passport-stub">Passport Stub</a></li>
<li><a href="#redis-stub" id="markdown-toc-redis-stub">Redis Stub</a></li>
<li><a href="#ensure-authenticated-stub" id="markdown-toc-ensure-authenticated-stub">Ensure Authenticated Stub</a></li>
<li><a href="#ensure-admin-stub" id="markdown-toc-ensure-admin-stub">Ensure Admin Stub</a></li>
<li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you will be able to…</p>
<ol>
<li>Discuss the overall client/server authentication workflow</li>
<li>Describe what a stub is and why you would want to use them in your test suites</li>
<li>Add Sinon to an existing Mocha test structure</li>
<li>Refactor each of the auth integration tests, stubbing Passport-related authentication middleware and calls to Postgres and Redis</li>
</ol>
<h2 id="project-setup">Project Setup</h2>
<p>Clone the <a href="https://github.com/mjhea0/node-koa-api">node-koa-api</a> repo (if you haven’t already), and then check out the <a href="https://github.com/mjhea0/node-koa-api/releases/tag/v3">v3</a> tag to the master branch and install the dependencies:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/mjhea0/node-koa-api <span class="se">\</span>
<span class="nt">--branch</span> v3 <span class="nt">--single-branch</span>
<span class="nv">$ </span><span class="nb">cd </span>node-koa-api
<span class="nv">$ </span>git checkout tags/v3 <span class="nt">-b</span> master
<span class="nv">$ </span>npm install
</code></pre></div></div>
<p>Review the project structure:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── knexfile.js
├── package.json
├── src
│ └── server
│ ├── auth.js
│ ├── db
│ │ ├── connection.js
│ │ ├── migrations
│ │ │ ├── 20170817152841_movies.js
│ │ │ └── 20171231115201_users.js
│ │ ├── queries
│ │ │ ├── movies.js
│ │ │ └── users.js
│ │ └── seeds
│ │ ├── movies_seed.js
│ │ └── users.js
│ ├── index.js
│ ├── routes
│ │ ├── auth.js
│ │ ├── index.js
│ │ └── movies.js
│ └── views
│ ├── login.html
│ ├── register.html
│ └── status.html
└── <span class="nb">test</span>
├── routes.auth.test.js
├── routes.index.test.js
├── routes.movies.test.js
└── sample.test.js
</code></pre></div></div>
<p>Take note of the routes as well:</p>
<table>
<thead>
<tr>
<th>URL</th>
<th>HTTP Verb</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>/</td>
<td>GET</td>
<td>Sanity Check</td>
</tr>
<tr>
<td>/api/v1/movies</td>
<td>GET</td>
<td>Return ALL movies</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>GET</td>
<td>Return a SINGLE movie</td>
</tr>
<tr>
<td>/api/v1/movies</td>
<td>POST</td>
<td>Add a movie</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>PUT</td>
<td>Update a movie</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>DELETE</td>
<td>Delete a movie</td>
</tr>
<tr>
<td>/auth/register</td>
<td>GET</td>
<td>Render the register view</td>
</tr>
<tr>
<td>/auth/register</td>
<td>POST</td>
<td>Register a new user</td>
</tr>
<tr>
<td>/auth/login</td>
<td>GET</td>
<td>Render the login view</td>
</tr>
<tr>
<td>/auth/login</td>
<td>POST</td>
<td>Log a user in</td>
</tr>
<tr>
<td>/auth/status</td>
<td>GET</td>
<td>Render the status page</td>
</tr>
<tr>
<td>/auth/logout</td>
<td>GET</td>
<td>Log a user out</td>
</tr>
</tbody>
</table>
<p>In short, this is a basic RESTful API with local authentication, powered by Koa and Passport.</p>
<blockquote>
<p>Want to learn how to build this project? Review the <a href="http://mherman.org/blog/2017/08/23/building-a-restful-api-with-koa-and-postgres">Building a RESTful API With Koa and Postgres</a> and <a href="http://mherman.org/blog/2018/01/02/user-authentication-with-passport-and-koa">User Authentication with Passport and Koa</a> blog posts.</p>
</blockquote>
<p>Moving on, <a href="https://www.postgresql.org/download/">download</a> and install Postgres (if necessary). Then, spin up the server on port 5432. Open psql, in your terminal and create two new databases:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>psql
<span class="c"># CREATE DATABASE koa_api;</span>
CREATE DATABASE
<span class="c"># CREATE DATABASE koa_api_test;</span>
CREATE DATABASE
<span class="c"># \q</span>
</code></pre></div></div>
<p>Spin up Redis in a new terminal tab:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>redis-server
</code></pre></div></div>
<p>Ensure the tests pass:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm test
Server listening on port: 1337
routes : auth
GET /auth/register
✓ should render the register view
POST /auth/register
✓ should register a new user (241ms)
GET /auth/login
✓ should render the login view
POST /auth/login
✓ should login a user (100ms)
routes : index
GET /
✓ should return json
routes : movies
GET /api/v1/movies
✓ should return all movies
GET /api/v1/movies/:id
✓ should respond with a single movie
✓ should throw an error if the movie does not exist
POST /api/v1/movies
✓ should return the movie that was added
✓ should throw an error if the payload is malformed
PUT /api/v1/movies
✓ should return the movie that was updated
✓ should throw an error if the movie does not exist
DELETE /api/v1/movies/:id
✓ should return the movie that was deleted
✓ should throw an error if the movie does not exist
Sample Test
✓ should pass
15 passing (3s)
</code></pre></div></div>
<p>Apply the migrations, and seed the database:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knex migrate:latest <span class="nt">--env</span> development
<span class="nv">$ </span>knex seed:run <span class="nt">--env</span> development
</code></pre></div></div>
<p>Run the Koa server, via <code class="highlighter-rouge">npm start</code>, and test out the following routes to get a feel for the app’s current functionality:</p>
<ol>
<li>http://localhost:1337/</li>
<li>http://localhost:1337/api/v1/movies</li>
<li>http://localhost:1337/auth/register</li>
<li>http://localhost:1337/auth/login</li>
<li>http://localhost:1337/auth/status</li>
<li>http://localhost:1337/auth/logout</li>
</ol>
<p>Install Sinon:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install sinon@4.1.5 <span class="nt">--save-dev</span>
</code></pre></div></div>
<h2 id="test-structure">Test Structure</h2>
<p>Let’s quickly update the test structure to differentiate between unit and integration tests and add a new file for the stubbed tests.</p>
<p>Add two new folders to the “test” directory:</p>
<ol>
<li>“unit”</li>
<li>“integration”</li>
</ol>
<p>Then, move the <em>sample.test.js</em> to the “unit” folder and the remaining test files to the “integration” folder.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>└── <span class="nb">test</span>
├── integration
│ ├── routes.auth.test.js
│ ├── routes.index.test.js
│ └── routes.movies.test.js
└── unit
└── sample.test.js
</code></pre></div></div>
<p>Update the <code class="highlighter-rouge">test</code> command in <em>package.json</em> so that the tests are discovered in the newly created folders:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"test": "./node_modules/mocha/bin/_mocha ./test/**/*.test.js"
</code></pre></div></div>
<p>You’ll need to update the paths in each of the test files as well:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../../src/server/index'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">knex</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../../src/server/db/connection'</span><span class="p">);</span>
</code></pre></div></div>
<p>Ensure the tests still pass before moving on.</p>
<p>Next, create a duplicate of <em>test/integration/routes.auth.test.js</em> called <em>test/integration/routes.auth.stub.test.js</em>.</p>
<p>Then, update the first <code class="highlighter-rouge">describe</code> block along with the <code class="highlighter-rouge">beforeEach</code> and <code class="highlighter-rouge">afterEach</code>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">.</span><span class="nx">only</span><span class="p">(</span><span class="s1">'routes : auth - stubbed'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="p">...</span>
<span class="p">});</span>
</code></pre></div></div>
<p>So, we’re no longer applying the migrations and only (via <code class="highlighter-rouge">.only</code>) running the tests in this <code class="highlighter-rouge">describe</code> block during test runs.</p>
<p>Run the tests:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm <span class="nb">test</span>
</code></pre></div></div>
<p>As expected, you should see an error since the database tables do not exist:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>error: relation <span class="s2">"users"</span> does not exist
</code></pre></div></div>
<h2 id="why-stub">Why Stub?</h2>
<p>Before beginning, review the following two sections from the <a href="http://mherman.org/blog/2017/11/06/stubbing-http-requests-with-sinon">Stubbing HTTP Requests with Sinon</a> blog post to get an overview of stubbing:</p>
<ol>
<li><a href="http://mherman.org/blog/2017/11/06/stubbing-http-requests-with-sinon/#what-is-a-stub">What is a Stub?</a></li>
<li><a href="http://mherman.org/blog/2017/11/06/stubbing-http-requests-with-sinon/#why-stub">Why Stub?</a></li>
</ol>
<p>The end goal of this post is to be able to test routes that require authentication <em>without</em> actually going through either of the following authentication flows…</p>
<blockquote>
<p>Be sure to review the actual code as you’re reading through each flow.</p>
</blockquote>
<p><em>First flow:</em></p>
<ol>
<li>User submits auth credentials (username and password), which are then sent to the server via a POST request to <code class="highlighter-rouge">/auth/login</code>.</li>
<li><code class="highlighter-rouge">passport.authenticate()</code> is called and the credentials are checked against the user info stored in the database.</li>
<li>If the credentials are correct, <code class="highlighter-rouge">passport.serializeUser()</code> is fired and the user <code class="highlighter-rouge">id</code> is serialized to the Redis session store via the <code class="highlighter-rouge">ctx.login()</code> method.</li>
<li>Finally, a cookie is generated, which is sent back with the response to the client and that cookie is then set.</li>
</ol>
<p><em>Second flow:</em></p>
<ol>
<li>An authenticated user hits a route requiring a user to be authenticated (like <code class="highlighter-rouge">/auth/status</code>).</li>
<li><code class="highlighter-rouge">isAuthenticated()</code> is called, which then verifies that (a) a user is in session and (b) that user is found within the database (via <code class="highlighter-rouge">passport.deserializeUser()</code>).</li>
<li>If correct, the end user is allowed to view the route and the appropriate response is sent.</li>
</ol>
<div style="text-align:left;padding-top:10px;padding-left:10px;">
<img src="/assets/img/blog/koa/client-server-auth-flow.png" style="max-width: 90%;border:0;box-shadow:none;" alt="client/server auth flow" />
</div>
<p><br /></p>
<p>Let’s get to it!</p>
<h2 id="postgres-stub">Postgres Stub</h2>
<p>We’ll start by stubbing the Postgres <code class="highlighter-rouge">addUser</code> query (which is a promise) called in the POST <code class="highlighter-rouge">/auth/register</code> route handler in <em>src/server/routes/auth.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">queries</span><span class="p">.</span><span class="nx">addUser</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span>
</code></pre></div></div>
<p>Think about how this should normally resolve. Something like this, right?</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
{
id: 1,
username: 'michael',
password: 'something'
}
];
</code></pre></div></div>
<p>Update the test like so:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">.</span><span class="nx">only</span><span class="p">(</span><span class="s1">'POST /auth/register'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">id</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'michael'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">'something'</span>
<span class="p">}</span>
<span class="p">];</span>
<span class="k">this</span><span class="p">.</span><span class="nx">query</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">queries</span><span class="p">,</span> <span class="s1">'addUser'</span><span class="p">).</span><span class="nx">resolves</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">query</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should register a new user'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'/auth/register'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">send</span><span class="p">({</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'michael'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">'herman'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Here, we stubbed the <code class="highlighter-rouge">addUser</code> query in the <code class="highlighter-rouge">beforeEach()</code>, passing in the expected <code class="highlighter-rouge">user</code> info for when the query is resolved, and then restored the functionality in the <code class="highlighter-rouge">afterEach()</code>.</p>
<p>Add the imports to the top:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">sinon</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'sinon'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">queries</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../../src/server/db/queries/users'</span><span class="p">);</span>
</code></pre></div></div>
<p>The test should fail since we’re still accessing the database in the Passport <code class="highlighter-rouge">authenticate</code> method.</p>
<h2 id="passport-stub">Passport Stub</h2>
<p>We can use a similar pattern for stubbing the <code class="highlighter-rouge">authenticate</code> method:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">passport</span><span class="p">,</span> <span class="s1">'authenticate'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Add this to the outer <code class="highlighter-rouge">describe</code> block so it’s applied to all tests. Then, to simulate the calling of the callback, update the <code class="highlighter-rouge">beforeEach</code> in the nested <code class="highlighter-rouge">describe</code>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">id</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'michael'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">'something'</span>
<span class="p">}</span>
<span class="p">];</span>
<span class="k">this</span><span class="p">.</span><span class="nx">query</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">queries</span><span class="p">,</span> <span class="s1">'addUser'</span><span class="p">).</span><span class="nx">resolves</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">1</span> <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>You should have something similar to:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">.</span><span class="nx">only</span><span class="p">(</span><span class="s1">'routes : auth - stubbed'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">passport</span><span class="p">,</span> <span class="s1">'authenticate'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">...</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'POST /auth/register'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">id</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'michael'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">'something'</span>
<span class="p">}</span>
<span class="p">];</span>
<span class="k">this</span><span class="p">.</span><span class="nx">query</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">queries</span><span class="p">,</span> <span class="s1">'addUser'</span><span class="p">).</span><span class="nx">resolves</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">1</span> <span class="p">});</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">query</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should register a new user'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'/auth/register'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">send</span><span class="p">({</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'michael'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">'herman'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">...</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Add the <code class="highlighter-rouge">beforeEach</code> to the POST <code class="highlighter-rouge">/auth/login</code> test as well:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'POST /auth/login'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">1</span> <span class="p">});</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should login a user'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">send</span><span class="p">({</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'jeremy'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">'johnson'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Run the tests:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm test
Server listening on port: 1337
routes : auth - stubbed
GET /auth/register
✓ should render the register view
POST /auth/register
✓ should register a new user
GET /auth/login
✓ should render the login view
POST /auth/login
✓ should login a user
</code></pre></div></div>
<p>They should all pass!</p>
<p>Notice how we passed in some dummy data to the callback, <code class="highlighter-rouge">null, { id: 1 }</code>. What if we passed <code class="highlighter-rouge">false</code> in for the second parameter instead of <code class="highlighter-rouge">{ id: 1 }</code>? Review the following code in <em>src/server/routes/auth.js</em> for more info:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">passport</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="k">new</span> <span class="nx">LocalStrategy</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">(</span><span class="nx">username</span><span class="p">,</span> <span class="nx">password</span><span class="p">,</span> <span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'users'</span><span class="p">).</span><span class="nx">where</span><span class="p">({</span> <span class="nx">username</span> <span class="p">}).</span><span class="nx">first</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">user</span><span class="p">)</span> <span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">comparePass</span><span class="p">(</span><span class="nx">password</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">password</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">user</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
<span class="p">}));</span>
</code></pre></div></div>
<p>How would you write a test, and stub it properly, for a situation where the user is found but the password is incorrect?</p>
<p>Add a new test:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'POST /auth/login'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should not login a user if the password is incorrect'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">send</span><span class="p">({</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'jeremy'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">'notcorrect'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">.</span><span class="nx">length</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">400</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'error'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Update the <code class="highlighter-rouge">serializeUser</code> and <code class="highlighter-rouge">deserializeUser</code> methods too:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">passport</span><span class="p">,</span> <span class="s1">'authenticate'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="k">this</span><span class="p">.</span><span class="nx">serialize</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">passport</span><span class="p">,</span> <span class="s1">'serializeUser'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="k">this</span><span class="p">.</span><span class="nx">deserialize</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">passport</span><span class="p">,</span> <span class="s1">'deserializeUser'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(</span>
<span class="p">()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">serialize</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">deserialize</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Add the appropriate yields:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="p">.</span><span class="nx">serialize</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">1</span> <span class="p">});</span>
<span class="k">this</span><span class="p">.</span><span class="nx">deserialize</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">1</span> <span class="p">});</span>
</code></pre></div></div>
<p>Ensure the tests still pass. Then, bring down the Postgres server and run them again. Again, they should pass.</p>
<p>It’s worth noting that these tests will run faster than the full integration flavor in <em>test/integration/routes.auth.test.js</em> since we’re no longer hitting Postgres - 82ms vs 1s. This may not be significant now, but think if the test suite had hundreds of tests all hitting the database - stubbing becomes super important for developer productivity.</p>
<h2 id="redis-stub">Redis Stub</h2>
<p>Kill the Redis server. Try running the tests. You should see a few failures since <code class="highlighter-rouge">ctx.login();</code> adds the session to Redis:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1) routes : auth - stubbed POST /auth/register should register a new user:
Error: Timeout of 2000ms exceeded.
For async tests and hooks, ensure "done()" is called;
if returning a Promise, ensure it resolves.
2) routes : auth - stubbed POST /auth/login should login a user:
Error: Timeout of 2000ms exceeded.
For async tests and hooks, ensure "done()" is called;
if returning a Promise, ensure it resolves.
</code></pre></div></div>
<p>To stub, let’s refactor out the initialization of the session store in <em>src/server/index.js</em>.</p>
<p>Add a new file to “src/server” called <em>session.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">RedisStore</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-redis'</span><span class="p">);</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">RedisStore</span><span class="p">();</span>
</code></pre></div></div>
<p>Then, within <em>src/server/index.js</em>, replace:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">RedisStore</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-redis'</span><span class="p">);</span>
</code></pre></div></div>
<p>With:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">store</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./session'</span><span class="p">);</span>
</code></pre></div></div>
<p>Update the mounting of the session to the middleware:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// sessions</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">keys</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'super-secret-key'</span><span class="p">];</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">session</span><span class="p">({</span> <span class="nx">store</span> <span class="p">},</span> <span class="nx">app</span><span class="p">));</span>
</code></pre></div></div>
<p>Fire the Redis server back up and run the tests to ensure that we didn’t break the existing functionality. Once done, kill the Redis server again and update the <code class="highlighter-rouge">beforeEach</code> and <code class="highlighter-rouge">afterEach</code> blocks:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">store</span><span class="p">,</span> <span class="s1">'set'</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">passport</span><span class="p">,</span> <span class="s1">'authenticate'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="k">this</span><span class="p">.</span><span class="nx">serialize</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">passport</span><span class="p">,</span> <span class="s1">'serializeUser'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="k">this</span><span class="p">.</span><span class="nx">deserialize</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">passport</span><span class="p">,</span> <span class="s1">'deserializeUser'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(</span>
<span class="p">()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">serialize</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">deserialize</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Import:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">store</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../../src/server/session'</span><span class="p">);</span>
</code></pre></div></div>
<p>The tests should now pass:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm test
Server listening on port: 1337
routes : auth - stubbed
GET /auth/register
✓ should render the register view
POST /auth/register
✓ should register a new user
GET /auth/login
✓ should render the login view
POST /auth/login
✓ should login a user
POST /auth/login
✓ should not login a user if the password is incorrect
</code></pre></div></div>
<p>We can also stub the <code class="highlighter-rouge">get</code> and <code class="highlighter-rouge">destroy</code> <a href="https://github.com/koajs/session#external-session-stores">methods</a> from <a href="https://github.com/koajs/session">koa-session</a>.</p>
<h2 id="ensure-authenticated-stub">Ensure Authenticated Stub</h2>
<p>Moving right along, let’s stub out the <code class="highlighter-rouge">ctx.isAuthenticated()</code> method. Again, we’ll need to do a quick refactor first.</p>
<p>First, add a new file to “src/server/routes/” called <em>_helpers.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">ensureAuthenticated</span><span class="p">(</span><span class="nx">context</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">context</span><span class="p">.</span><span class="nx">isAuthenticated</span><span class="p">();</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">ensureAuthenticated</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Import the function into <em>src/server/routes/auth.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">helpers</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./_helpers'</span><span class="p">);</span>
</code></pre></div></div>
<p>Refactor the GET <code class="highlighter-rouge">/auth/status</code>, GET <code class="highlighter-rouge">/auth/logout</code>, and GET <code class="highlighter-rouge">/auth/login</code> routes to use the new helper:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">helpers</span><span class="p">.</span><span class="nx">ensureAuthenticated</span><span class="p">())</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">type</span> <span class="o">=</span> <span class="s1">'html'</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">createReadStream</span><span class="p">(</span><span class="s1">'./src/server/views/login.html'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/logout'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">helpers</span><span class="p">.</span><span class="nx">ensureAuthenticated</span><span class="p">(</span><span class="nx">ctx</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">logout</span><span class="p">();</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span> <span class="na">success</span><span class="p">:</span> <span class="kc">false</span> <span class="p">};</span>
<span class="nx">ctx</span><span class="p">.</span><span class="k">throw</span><span class="p">(</span><span class="mi">401</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">helpers</span><span class="p">.</span><span class="nx">ensureAuthenticated</span><span class="p">(</span><span class="nx">ctx</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">type</span> <span class="o">=</span> <span class="s1">'html'</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">createReadStream</span><span class="p">(</span><span class="s1">'./src/server/views/status.html'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Import the function again into the tests:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">helpers</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../../src/server/routes/_helpers'</span><span class="p">);</span>
</code></pre></div></div>
<p>Create the stub:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">ensureAuthenticated</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span>
<span class="nx">helpers</span><span class="p">,</span> <span class="s1">'ensureAuthenticated'</span>
<span class="p">).</span><span class="nx">returns</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">store</span><span class="p">,</span> <span class="s1">'set'</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">passport</span><span class="p">,</span> <span class="s1">'authenticate'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="k">this</span><span class="p">.</span><span class="nx">serialize</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">passport</span><span class="p">,</span> <span class="s1">'serializeUser'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="k">this</span><span class="p">.</span><span class="nx">deserialize</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">passport</span><span class="p">,</span> <span class="s1">'deserializeUser'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(</span>
<span class="p">()</span> <span class="o">=></span> <span class="p">{});</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">serialize</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">deserialize</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">ensureAuthenticated</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Add the tests:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /auth/status'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">ensureAuthenticated</span><span class="p">.</span><span class="nx">returns</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should render the status view'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">.</span><span class="nx">length</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'text/html'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'<p>You are authenticated.</p>'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /auth/login'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">ensureAuthenticated</span><span class="p">.</span><span class="nx">returns</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should render the status view if the user is logged in'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">.</span><span class="nx">length</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'text/html'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'<p>You are authenticated.</p>'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Update the other GET <code class="highlighter-rouge">/auth/login</code> test:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /auth/login'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">ensureAuthenticated</span><span class="p">.</span><span class="nx">returns</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should render the login view if a user is not logged in'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">.</span><span class="nx">length</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'text/html'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'<h1>Login</h1>'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span>
<span class="s1">'<p><button type="submit">Log In</button></p>'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>All tests should pass!</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm test
Server listening on port: 1337
routes : auth - stubbed
GET /auth/register
✓ should render the register view
POST /auth/register
✓ should register a new user
GET /auth/login
✓ should render the login view if a user is not logged in
POST /auth/login
✓ should login a user
POST /auth/login
✓ should not login a user if the password is incorrect
GET /auth/status
✓ should render the status view
GET /auth/login
✓ should render the status view if the user is logged in
</code></pre></div></div>
<p>Spin the Postgres and Redis servers back up, remove the <code class="highlighter-rouge">.only</code> from the describe, and run <em>all</em> the tests:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm test
Server listening on port: 1337
routes : auth - stubbed
GET /auth/register
✓ should render the register view
POST /auth/register
✓ should register a new user
GET /auth/login
✓ should render the login view if a user is not logged in
POST /auth/login
✓ should login a user
POST /auth/login
✓ should not login a user if the password is incorrect
GET /auth/status
✓ should render the status view
GET /auth/login
✓ should render the status view if the user is logged in
routes : auth
GET /auth/register
✓ should render the register view
POST /auth/register
✓ should register a new user (200ms)
GET /auth/login
✓ should render the login view
POST /auth/login
✓ should login a user (99ms)
routes : index
GET /
✓ should return json
routes : movies
GET /api/v1/movies
✓ should return all movies
GET /api/v1/movies/:id
✓ should respond with a single movie
✓ should throw an error if the movie does not exist
POST /api/v1/movies
✓ should return the movie that was added
✓ should throw an error if the payload is malformed
PUT /api/v1/movies
✓ should return the movie that was updated
✓ should throw an error if the movie does not exist
DELETE /api/v1/movies/:id
✓ should return the movie that was deleted
✓ should throw an error if the movie does not exist
Sample Test
✓ should pass
22 passing (3s)
</code></pre></div></div>
<h2 id="ensure-admin-stub">Ensure Admin Stub</h2>
<p>Check your understanding by adding an <code class="highlighter-rouge">ensureAdmin</code> method to prevent access to a route if a user is not an admin. Add a route handler as well.</p>
<p><strong>Steps:</strong></p>
<ol>
<li>Write a test</li>
<li>Ensure the test fails</li>
<li>Update the <code class="highlighter-rouge">users</code> model in the migration file, <em>src/server/db/migrations/20170817152841_movies.js</em></li>
<li>Update the users seed</li>
<li>Apply the migration and seed</li>
<li>Add a route handler</li>
<li>Add the HTML view</li>
<li>Add the <code class="highlighter-rouge">ensureAdmin</code> helper</li>
<li>Manually test the route in the browser</li>
<li>Ensure the test still fails</li>
<li>Add the stub to the test</li>
<li>Ensure the test passes</li>
</ol>
<p>Compare your solution with the code in the <a href="https://github.com/mjhea0/node-koa-api">repo</a>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This article took a look at how to stub authentication middleware and database calls with Sinon.</p>
<p>The full code can be found in the <a href="https://github.com/mjhea0/node-koa-api/releases/tag/v4">v4</a> tag of the <a href="https://github.com/mjhea0/node-koa-api">node-koa-api</a> repository. Please share your comments, questions, and/or tips below.</p>
Michael Herman
Last time we looked at how to set up Passport local authentication with Node and Koa. We took a test-first approach and wrote the majority of tests first. That said, there were a two routes that we could not test (/auth/status and /auth/logout) since they required us to to bypass the isAuthenticated() method and manually set a cookie.
User Authentication with Passport and Koa
2018-01-02T00:00:00-06:00
2018-01-02T00:00:00-06:00
https://mherman.org/blog/user-authentication-with-passport-and-koa
<p><a href="http://www.passportjs.org/">Passport</a> is a library that provides a simple authentication middleware for Node.js.</p>
<p>This tutorial looks at how to set up a local authentication strategy with Node, Koa, and <a href="https://github.com/rkusa/koa-passport">koa-passport</a>, where users can sign up and log in using a username and password. We’ll also use Postgres for storing user information and Redis for session management.</p>
<div style="text-align:center;">
<img src="/assets/img/blog/koa/node-koa-passport.png" style="max-width: 90%; border:0; box-shadow: none;" alt="node koa passport" />
</div>
<p><br /></p>
<p><em>Last updated on July, 25 2018 to update a failing test.</em></p>
<h4 id="parts">Parts</h4>
<p>This article is part of a 4-part Koa and Sinon series…</p>
<ol>
<li><a href="http://mherman.org/blog/2017/08/23/building-a-restful-api-with-koa-and-postgres">Building a RESTful API with Koa and Postgres</a></li>
<li><a href="http://mherman.org/blog/2017/11/06/stubbing-http-requests-with-sinon">Stubbing HTTP Requests with Sinon</a></li>
<li><a href="http://mherman.org/blog/2018/01/02/user-authentication-with-passport-and-koa">User Authentication with Passport and Koa</a> (this article)</li>
<li><a href="http://mherman.org/blog/2018/01/22/stubbing-node-authentication-middleware-with-sinon">Stubbing Node Authentication Middleware with Sinon</a></li>
</ol>
<h4 id="main-npm-dependencies">Main NPM Dependencies</h4>
<ol>
<li>Koa v<a href="https://github.com/koajs/koa/releases/tag/2.3.0">2.3.0</a></li>
<li>Mocha v<a href="https://github.com/mochajs/mocha/releases/tag/v3.5.0">3.5.0</a></li>
<li>Chai v<a href="https://github.com/chaijs/chai/releases/tag/4.1.1">4.1.1</a></li>
<li>Chai HTTP v<a href="https://github.com/chaijs/chai-http/releases/tag/3.0.0">3.0.0</a></li>
<li>Knex v<a href="https://github.com/tgriesser/knex/releases/tag/0.13.0">0.13.0</a></li>
<li>pg v<a href="https://github.com/brianc/node-postgres/releases/tag/v7.1.2">7.1.2</a></li>
<li>koa-router v<a href="https://github.com/alexmingoia/koa-router/releases/tag/v7.2.1">7.2.1</a></li>
<li>koa-bodyparser v<a href="https://github.com/koajs/bodyparser/releases/tag/4.2.0">4.2.0</a></li>
<li>koa-passport v<a href="https://github.com/rkusa/koa-passport/releases/tag/4.0.1">4.0.1</a></li>
<li>koa-session v<a href="https://github.com/koajs/session/releases/tag/5.5.1">5.5.1</a></li>
<li>passport-local v<a href="https://github.com/jaredhanson/passport-local/releases/tag/v1.0.0">1.0.0</a></li>
<li>bcrypt.js v<a href="https://github.com/dcodeIO/bcrypt.js/releases/tag/2.4.3">2.4.3</a></li>
<li>koa-redis v<a href="https://github.com/koajs/koa-redis/releases/tag/v3.1.1">3.1.1</a></li>
</ol>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#user-model" id="markdown-toc-user-model">User Model</a></li>
<li><a href="#passport-setup" id="markdown-toc-passport-setup">Passport Setup</a></li>
<li><a href="#passport-local-strategy" id="markdown-toc-passport-local-strategy">Passport Local Strategy</a></li>
<li><a href="#routes-and-tests" id="markdown-toc-routes-and-tests">Routes and Tests</a></li>
<li><a href="#password-hashing" id="markdown-toc-password-hashing">Password Hashing</a></li>
<li><a href="#redis-session-store" id="markdown-toc-redis-session-store">Redis Session Store</a></li>
<li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you will be able to…</p>
<ol>
<li>Discuss the overall client/server authentication workflow</li>
<li>Add Passport and passport-local to a Koa app</li>
<li>Configure bcrypt.js for salting and hashing passwords</li>
<li>Practice test driven development</li>
<li>Register and authenticate a user</li>
<li>Utilize sessions to store user information via koa-session</li>
<li>Explain why you may want to use an external session store to store session data</li>
<li>Set up an external session store with Redis</li>
<li>Render HTML pages via server-side templating</li>
</ol>
<h2 id="project-setup">Project Setup</h2>
<p>Start by cloning down the base Koa project:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/mjhea0/node-koa-api <span class="se">\</span>
<span class="nt">--branch</span> v2 <span class="nt">--single-branch</span>
<span class="nv">$ </span><span class="nb">cd </span>node-koa-api
</code></pre></div></div>
<p>Then, check out the <a href="https://github.com/mjhea0/node-koa-api/releases/tag/v2">v2</a> tag to the master branch and install the dependencies:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git checkout tags/v2 <span class="nt">-b</span> master
<span class="nv">$ </span>npm install
</code></pre></div></div>
<p>Take a quick look at the code along with the project structure:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── knexfile.js
├── package.json
├── src
│ └── server
│ ├── db
│ │ ├── connection.js
│ │ ├── migrations
│ │ │ └── 20170817152841_movies.js
│ │ ├── queries
│ │ │ └── movies.js
│ │ └── seeds
│ │ └── movies_seed.js
│ ├── index.js
│ └── routes
│ ├── index.js
│ └── movies.js
└── <span class="nb">test</span>
├── routes.index.test.js
├── routes.movies.test.js
└── sample.test.js
</code></pre></div></div>
<p>This is just a basic RESTful API, with the following routes:</p>
<table>
<thead>
<tr>
<th>URL</th>
<th>HTTP Verb</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>/api/v1/movies</td>
<td>GET</td>
<td>Return ALL movies</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>GET</td>
<td>Return a SINGLE movie</td>
</tr>
<tr>
<td>/api/v1/movies</td>
<td>POST</td>
<td>Add a movie</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>PUT</td>
<td>Update a movie</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>DELETE</td>
<td>Delete a movie</td>
</tr>
</tbody>
</table>
<blockquote>
<p>Want to learn how to build this project? Review the <a href="http://mherman.org/blog/2017/08/23/building-a-restful-api-with-koa-and-postgres">Building a RESTful API With Koa and Postgres</a> blog post.</p>
</blockquote>
<p><a href="https://www.postgresql.org/download/">Download</a> and install Postgres (if necessary), and then fire up the server on port 5432. Open psql, in the terminal, and create two new databases, one for development and the other for testing:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>psql
<span class="c"># CREATE DATABASE koa_api;</span>
CREATE DATABASE
<span class="c"># CREATE DATABASE koa_api_test;</span>
CREATE DATABASE
<span class="c"># \q</span>
</code></pre></div></div>
<p>Ensure the tests pass:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm test
Server listening on port: 1337
routes : index
GET /
✓ should return json
routes : movies
GET /api/v1/movies
✓ should return all movies
GET /api/v1/movies/:id
✓ should respond with a single movie
✓ should throw an error if the movie does not exist
POST /api/v1/movies
✓ should return the movie that was added
✓ should throw an error if the payload is malformed
PUT /api/v1/movies
✓ should return the movie that was updated
✓ should throw an error if the movie does not exist
DELETE /api/v1/movies/:id
✓ should return the movie that was deleted
✓ should throw an error if the movie does not exist
Sample Test
✓ should pass
11 passing (624ms)
</code></pre></div></div>
<p>Apply the migrations, and seed the database:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knex migrate:latest <span class="nt">--env</span> development
<span class="nv">$ </span>knex seed:run <span class="nt">--env</span> development
</code></pre></div></div>
<p>Run the Koa server, via <code class="highlighter-rouge">npm start</code>, and navigate to <a href="http://localhost:1337/api/v1/movies">http://localhost:1337/api/v1/movies</a>. You should see something similar to:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w">
</span><span class="s2">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The Land Before Time"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Fantasy"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">7</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Jurassic Park"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Science Fiction"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Ice Age: Dawn of the Dinosaurs"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Action/Romance"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h2 id="user-model">User Model</h2>
<p>Generate a new migration template for the user model:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knex migrate:make users
</code></pre></div></div>
<p>Then update the newly created file:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">exports</span><span class="p">.</span><span class="nx">up</span> <span class="o">=</span> <span class="p">(</span><span class="nx">knex</span><span class="p">,</span> <span class="nb">Promise</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">createTable</span><span class="p">(</span><span class="s1">'users'</span><span class="p">,</span> <span class="p">(</span><span class="nx">table</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">table</span><span class="p">.</span><span class="nx">increments</span><span class="p">();</span>
<span class="nx">table</span><span class="p">.</span><span class="nx">string</span><span class="p">(</span><span class="s1">'username'</span><span class="p">).</span><span class="nx">unique</span><span class="p">().</span><span class="nx">notNullable</span><span class="p">();</span>
<span class="nx">table</span><span class="p">.</span><span class="nx">string</span><span class="p">(</span><span class="s1">'password'</span><span class="p">).</span><span class="nx">notNullable</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="nx">exports</span><span class="p">.</span><span class="nx">down</span> <span class="o">=</span> <span class="p">(</span><span class="nx">knex</span><span class="p">,</span> <span class="nb">Promise</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">dropTable</span><span class="p">(</span><span class="s1">'users'</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Apply the migration against the development database:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knex migrate:latest <span class="nt">--env</span> development
</code></pre></div></div>
<h2 id="passport-setup">Passport Setup</h2>
<p>Install the <a href="https://github.com/jaredhanson/passport">koa-passport</a> wrapper module:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install koa-passport@4.0.1 <span class="nt">--save</span>
</code></pre></div></div>
<p>Then, update <em>src/server/index.js</em> to add Passport to the app middleware along with <a href="https://github.com/koajs/session">koa-session</a>, which is used for managing sessions:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Koa</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">bodyParser</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-bodyparser'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">session</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-session'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">passport</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-passport'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">indexRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./routes/index'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">movieRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./routes/movies'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Koa</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">PORT</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span> <span class="o">||</span> <span class="mi">1337</span><span class="p">;</span>
<span class="c1">// sessions</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">keys</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'super-secret-key'</span><span class="p">];</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">session</span><span class="p">(</span><span class="nx">app</span><span class="p">));</span>
<span class="c1">// body parser</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">bodyParser</span><span class="p">());</span>
<span class="c1">// authentication</span>
<span class="nx">require</span><span class="p">(</span><span class="s1">'./auth'</span><span class="p">);</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">passport</span><span class="p">.</span><span class="nx">initialize</span><span class="p">());</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">passport</span><span class="p">.</span><span class="nx">session</span><span class="p">());</span>
<span class="c1">// routes</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">indexRoutes</span><span class="p">.</span><span class="nx">routes</span><span class="p">());</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">movieRoutes</span><span class="p">.</span><span class="nx">routes</span><span class="p">());</span>
<span class="c1">// server</span>
<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">PORT</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Server listening on port: </span><span class="p">${</span><span class="nx">PORT</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">server</span><span class="p">;</span>
</code></pre></div></div>
<blockquote>
<p>In production, make sure to update the secret key, <code class="highlighter-rouge">app.keys</code>. For example, you can use Python to generate a secure key:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>python3
<span class="o">>></span> import os
<span class="o">>></span> os.urandom<span class="o">(</span>24<span class="o">)</span>
b<span class="s1">'3\xa5\xfa\xc6\xfb\x0e\x1dA\x19-U\x15Y\x9e2]\x92/\x97\x8d\xecsJ\xb7'</span>
</code></pre></div> </div>
</blockquote>
<p>Install koa-session:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install koa-session@5.5.1 <span class="nt">--save</span>
</code></pre></div></div>
<p>Sessions are stored in a cookie by default on the client-side, unencrypted. We’ll stick with this for now, just to get things up and running, but we’ll refactor and add Redis before all is said and done.</p>
<p>Before moving on, let’s handle <a href="https://github.com/jaredhanson/passport#sessions">serializing and de-serializing the user information to the session</a>. Create a new file called <em>auth.js</em> in “src/server”:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">passport</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-passport'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">knex</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./db/connection'</span><span class="p">);</span>
<span class="nx">passport</span><span class="p">.</span><span class="nx">serializeUser</span><span class="p">((</span><span class="nx">user</span><span class="p">,</span> <span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span> <span class="p">});</span>
<span class="nx">passport</span><span class="p">.</span><span class="nx">deserializeUser</span><span class="p">((</span><span class="nx">id</span><span class="p">,</span> <span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'users'</span><span class="p">).</span><span class="nx">where</span><span class="p">({</span><span class="nx">id</span><span class="p">}).</span><span class="nx">first</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">user</span><span class="p">);</span> <span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">done</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span><span class="kc">null</span><span class="p">);</span> <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<h2 id="passport-local-strategy">Passport Local Strategy</h2>
<p>Next, install the <a href="https://github.com/jaredhanson/passport-local">passport-local</a> strategy, which is used for authenticating with a username and password:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install passport-local@1.0.0 <span class="nt">--save</span>
</code></pre></div></div>
<p>Update <em>auth.js</em> like so:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">passport</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-passport'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">LocalStrategy</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'passport-local'</span><span class="p">).</span><span class="nx">Strategy</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">knex</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./db/connection'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nx">passport</span><span class="p">.</span><span class="nx">serializeUser</span><span class="p">((</span><span class="nx">user</span><span class="p">,</span> <span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span> <span class="p">});</span>
<span class="nx">passport</span><span class="p">.</span><span class="nx">deserializeUser</span><span class="p">((</span><span class="nx">id</span><span class="p">,</span> <span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'users'</span><span class="p">).</span><span class="nx">where</span><span class="p">({</span><span class="nx">id</span><span class="p">}).</span><span class="nx">first</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">user</span><span class="p">);</span> <span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">done</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span><span class="kc">null</span><span class="p">);</span> <span class="p">});</span>
<span class="p">});</span>
<span class="nx">passport</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="k">new</span> <span class="nx">LocalStrategy</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">(</span><span class="nx">username</span><span class="p">,</span> <span class="nx">password</span><span class="p">,</span> <span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'users'</span><span class="p">).</span><span class="nx">where</span><span class="p">({</span> <span class="nx">username</span> <span class="p">}).</span><span class="nx">first</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">user</span><span class="p">)</span> <span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">password</span> <span class="o">===</span> <span class="nx">user</span><span class="p">.</span><span class="nx">password</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">user</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
<span class="p">}));</span>
</code></pre></div></div>
<p>Here, we check if the user exists and the password matches what’s in the database and then pass the results back to Passport via the callback:</p>
<ul>
<li><em>Does the username exist?</em>
<ul>
<li>No? then <code class="highlighter-rouge">false</code> is returned</li>
<li>Yes? <em>Does the password match?</em>
<ul>
<li>No? <code class="highlighter-rouge">false</code> is returned</li>
<li>Yes? The user object is returned and then the <code class="highlighter-rouge">id</code> is serialized to the session</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>You probably noticed that we are checking that the provided password is literally the same as the password pulled from the database, so we are storing the password in plain text. We’ll update this after we add the main routes.</p>
</blockquote>
<h2 id="routes-and-tests">Routes and Tests</h2>
<p>Like the majority of my tutorials, we’ll write tests first. That said, we will <em>only</em> be testing the happy paths. It’s up to you to add tests for handling errors.</p>
<p>Routes:</p>
<table>
<thead>
<tr>
<th>URL</th>
<th>HTTP Verb</th>
<th>Authenticated?</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>/auth/register</td>
<td>GET</td>
<td>No</td>
<td>Render the register view</td>
</tr>
<tr>
<td>/auth/register</td>
<td>POST</td>
<td>No</td>
<td>Register a new user</td>
</tr>
<tr>
<td>/auth/login</td>
<td>GET</td>
<td>No</td>
<td>Render the login view</td>
</tr>
<tr>
<td>/auth/login</td>
<td>POST</td>
<td>No</td>
<td>Log a user in</td>
</tr>
<tr>
<td>/auth/status</td>
<td>GET</td>
<td>Yes</td>
<td>Render the status page</td>
</tr>
<tr>
<td>/auth/logout</td>
<td>GET</td>
<td>Yes</td>
<td>Log a user out</td>
</tr>
</tbody>
</table>
<p>Full Authentication flow:</p>
<ol>
<li>The end user provides a username and a password and the credentials are sent to the server-side</li>
<li>The server-side Koa app then checks the credentials against the database
<ul>
<li>If they are correct, the end user is redirected to <code class="highlighter-rouge">/auth/status</code></li>
<li>If they are incorrect, the end user is redirected to <code class="highlighter-rouge">/auth/login</code></li>
</ul>
</li>
</ol>
<p>Create a new file called <em>routes.auth.test.js</em> in “test”:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">=</span> <span class="s1">'test'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">chai</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'chai'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">should</span> <span class="o">=</span> <span class="nx">chai</span><span class="p">.</span><span class="nx">should</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">chaiHttp</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'chai-http'</span><span class="p">);</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">chaiHttp</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../src/server/index'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">knex</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../src/server/db/connection'</span><span class="p">);</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'routes : auth'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">migrate</span><span class="p">.</span><span class="nx">rollback</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span> <span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">migrate</span><span class="p">.</span><span class="nx">latest</span><span class="p">();</span> <span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span> <span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">seed</span><span class="p">.</span><span class="nx">run</span><span class="p">();</span> <span class="p">});</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">migrate</span><span class="p">.</span><span class="nx">rollback</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>This is just a boilerplate for the tests.</p>
<h3 id="register---get">Register - GET</h3>
<p>This route serves up a view with an HTML form for users to register with.</p>
<h4 id="test">Test</h4>
<p>Start with a test:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /auth/register'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should render the register view'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/register'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">.</span><span class="nx">length</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'text/html'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'<h1>Register</h1>'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span>
<span class="s1">'<p><button type="submit">Register</button></p>'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Run the tests. You should see the following error:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Uncaught AssertionError: expected <span class="o">[</span>Error: Not Found] to not exist
</code></pre></div></div>
<p>Now let’s write the code to get it to pass…</p>
<h4 id="code">Code</h4>
<p>First, add a new file to the “src/server/routes” folder called <em>auth.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Router</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-router'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">passport</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-passport'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'fs'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">queries</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../db/queries/users'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">router</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Router</span><span class="p">();</span>
<span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/register'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">type</span> <span class="o">=</span> <span class="s1">'html'</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">createReadStream</span><span class="p">(</span><span class="s1">'./src/server/views/register.html'</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">router</span><span class="p">;</span>
</code></pre></div></div>
<p>Add another new file called <em>users.js</em> to “src/server/db/queries”. Leave the file empty for now. Create the “views” folder, and then add the <em>register.html</em> template:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html></span>
<span class="nt"><head></span>
<span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"utf-8"</span><span class="nt">></span>
<span class="nt"><title></span>Register<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><h1></span>Register<span class="nt"></h1></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"/auth/register"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
<span class="nt"><p><label></span>Username: <span class="nt"><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">name=</span><span class="s">"username"</span><span class="nt">/></label></p></span>
<span class="nt"><p><label></span>Password: <span class="nt"><input</span> <span class="na">type=</span><span class="s">"password"</span> <span class="na">name=</span><span class="s">"password"</span><span class="nt">/></label></p></span>
<span class="nt"><p><button</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>Register<span class="nt"></button></p></span>
<span class="nt"></form></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<p>Register the auth routes in <em>src/server/index.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Koa</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">bodyParser</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-bodyparser'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">session</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-session'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">passport</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-passport'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">indexRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./routes/index'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">movieRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./routes/movies'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">authRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./routes/auth'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Koa</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">PORT</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span> <span class="o">||</span> <span class="mi">1337</span><span class="p">;</span>
<span class="c1">// sessions</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">keys</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'super-secret-key'</span><span class="p">];</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">session</span><span class="p">(</span><span class="nx">app</span><span class="p">));</span>
<span class="c1">// body parser</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">bodyParser</span><span class="p">());</span>
<span class="c1">// authentication</span>
<span class="nx">require</span><span class="p">(</span><span class="s1">'./auth'</span><span class="p">);</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">passport</span><span class="p">.</span><span class="nx">initialize</span><span class="p">());</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">passport</span><span class="p">.</span><span class="nx">session</span><span class="p">());</span>
<span class="c1">// routes</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">indexRoutes</span><span class="p">.</span><span class="nx">routes</span><span class="p">());</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">movieRoutes</span><span class="p">.</span><span class="nx">routes</span><span class="p">());</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">authRoutes</span><span class="p">.</span><span class="nx">routes</span><span class="p">());</span>
<span class="c1">// server</span>
<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">PORT</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Server listening on port: </span><span class="p">${</span><span class="nx">PORT</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">server</span><span class="p">;</span>
</code></pre></div></div>
<p>Ensure the tests now pass:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm test
Server listening on port: 1337
routes : auth
GET /auth/register
✓ should render the register view
routes : index
GET /
✓ should return json
routes : movies
GET /api/v1/movies
✓ should return all movies
GET /api/v1/movies/:id
✓ should respond with a single movie
✓ should throw an error if the movie does not exist
POST /api/v1/movies
✓ should return the movie that was added
✓ should throw an error if the payload is malformed
PUT /api/v1/movies
✓ should return the movie that was updated
✓ should throw an error if the movie does not exist
DELETE /api/v1/movies/:id
✓ should return the movie that was deleted
✓ should throw an error if the movie does not exist
Sample Test
✓ should pass
12 passing (868ms)
</code></pre></div></div>
<h3 id="register---post">Register - POST</h3>
<h4 id="test-1">Test</h4>
<p>Again, start with a test:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'POST /auth/register'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should register a new user'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'/auth/register'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">send</span><span class="p">({</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'michael'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">'herman'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>The test should fail with the following error, since the route does not exist:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Uncaught AssertionError: expected <span class="o">[</span>Error: Not Found] to not exist
</code></pre></div></div>
<h4 id="code-1">Code</h4>
<p>Add the route handler:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'/auth/register'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">queries</span><span class="p">.</span><span class="nx">addUser</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">passport</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">(</span><span class="s1">'local'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">info</span><span class="p">,</span> <span class="nx">status</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">400</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="s1">'error'</span> <span class="p">};</span>
<span class="p">}</span>
<span class="p">})(</span><span class="nx">ctx</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Here, if the user is successfully added to the database, we call the <code class="highlighter-rouge">login</code> method, <a href="https://github.com/rkusa/koa-passport/blob/master/lib/framework/koa.js#L44">from koa-passport</a>, to trigger the creation of the session and then redirect the user to <code class="highlighter-rouge">/auth/status</code></p>
<p>For the <code class="highlighter-rouge">addUser()</code> helper, add the following code to <em>src/server/db/queries/users.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">knex</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../connection'</span><span class="p">);</span>
<span class="kd">function</span> <span class="nx">addUser</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'users'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">insert</span><span class="p">({</span>
<span class="na">username</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">username</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">password</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">returning</span><span class="p">(</span><span class="s1">'*'</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">addUser</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Ensure the tests pass.</p>
<h3 id="status">Status</h3>
<p>Add the route handler:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">isAuthenticated</span><span class="p">())</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">type</span> <span class="o">=</span> <span class="s1">'html'</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">createReadStream</span><span class="p">(</span><span class="s1">'./src/server/views/status.html'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>
<p>For this route, we’ll skip the tests since we’ll have to stub the <code class="highlighter-rouge">isAuthenticated()</code> method and manually set a cookie.</p>
<p>Add the template:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html></span>
<span class="nt"><head></span>
<span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"utf-8"</span><span class="nt">></span>
<span class="nt"><title></span>Status<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><p></span>You are authenticated.<span class="nt"></p></span>
<span class="nt"><p><a</span> <span class="na">href=</span><span class="s">"/auth/logout"</span><span class="nt">></span>Logout<span class="nt"></a></span>?<span class="nt"></p></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<p>To test, fire up the server via <code class="highlighter-rouge">npm start</code> and navigate to <a href="http://localhost:1337/auth/register">http://localhost:1337/auth/status</a>. Register a new user. You should be redirected to <code class="highlighter-rouge">auth/status</code> and a cookie should be set:</p>
<p><img src="/assets/img/blog/koa/koa-passport-status.png" alt="koa passport status" /></p>
<h3 id="login---get">Login - GET</h3>
<p>For this route, we’ll serve up a view with an HTML form for users to log in with.</p>
<h4 id="test-2">Test</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /auth/login'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should render the login view'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">.</span><span class="nx">length</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'text/html'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'<h1>Login</h1>'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span>
<span class="s1">'<p><button type="submit">Log In</button></p>'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<h4 id="code-2">Code</h4>
<p>Add the route handler:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">isAuthenticated</span><span class="p">())</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">type</span> <span class="o">=</span> <span class="s1">'html'</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">createReadStream</span><span class="p">(</span><span class="s1">'./src/server/views/login.html'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Then, add the <em>login.html</em> template:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html></span>
<span class="nt"><head></span>
<span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"utf-8"</span><span class="nt">></span>
<span class="nt"><title></span>Login<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><h1></span>Login<span class="nt"></h1></span>
<span class="nt"><form</span> <span class="na">action=</span><span class="s">"/auth/login"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
<span class="nt"><p><label></span>Username: <span class="nt"><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">name=</span><span class="s">"username"</span><span class="nt">/></label></p></span>
<span class="nt"><p><label></span>Password: <span class="nt"><input</span> <span class="na">type=</span><span class="s">"password"</span> <span class="na">name=</span><span class="s">"password"</span><span class="nt">/></label></p></span>
<span class="nt"><p><button</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">></span>Log In<span class="nt"></button></p></span>
<span class="nt"></form></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<p>The tests should now pass.</p>
<h3 id="login---post">Login - POST</h3>
<h4 id="test-3">Test</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'POST /auth/login'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should login a user'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">send</span><span class="p">({</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'jeremy'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">'johnson'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">redirects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<h4 id="code-3">Code</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">passport</span><span class="p">.</span><span class="nx">authenticate</span><span class="p">(</span><span class="s1">'local'</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">info</span><span class="p">,</span> <span class="nx">status</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="s1">'/auth/status'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">400</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="s1">'error'</span> <span class="p">};</span>
<span class="p">}</span>
<span class="p">})(</span><span class="nx">ctx</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Let’s also create a new Knex seed file to add a test user to the database:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knex seed:make users
</code></pre></div></div>
<p>Add the following code to the newly created seed in “src/server/db/seeds”:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">exports</span><span class="p">.</span><span class="nx">seed</span> <span class="o">=</span> <span class="p">(</span><span class="nx">knex</span><span class="p">,</span> <span class="nb">Promise</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'users'</span><span class="p">).</span><span class="nx">del</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'users'</span><span class="p">).</span><span class="nx">insert</span><span class="p">({</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'jeremy'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">'johnson'</span>
<span class="p">})</span>
<span class="p">);</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>The tests should now pass. Before moving on, try adding a few more tests to handle errors as well.</p>
<h3 id="logout">Logout</h3>
<p>We won’t be writing any tests for this route either since we’ll have to stub portions of the auth workflow. So, just add the route handler:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/auth/logout'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">isAuthenticated</span><span class="p">())</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">logout</span><span class="p">();</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="s1">'/auth/login'</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span> <span class="na">success</span><span class="p">:</span> <span class="kc">false</span> <span class="p">};</span>
<span class="nx">ctx</span><span class="p">.</span><span class="k">throw</span><span class="p">(</span><span class="mi">401</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Manually test by registering a new user. If all is well, you should be redirected to <code class="highlighter-rouge">auth/status</code> and a cookie should be set. Then ensure that the cookie is removed after you log out.</p>
<p>Make sure all tests pass before moving on.</p>
<h2 id="password-hashing">Password Hashing</h2>
<p>Install <a href="https://github.com/dcodeIO/bcrypt.js">bcrypt.js</a> to handle the salting and hashing of passwords:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install bcryptjs@2.4.3 <span class="nt">--save</span>
</code></pre></div></div>
<p>Start by adding a helper method called <code class="highlighter-rouge">comparePassword</code> to <em>src/server/auth.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">comparePass</span><span class="p">(</span><span class="nx">userPassword</span><span class="p">,</span> <span class="nx">databasePassword</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">bcrypt</span><span class="p">.</span><span class="nx">compareSync</span><span class="p">(</span><span class="nx">userPassword</span><span class="p">,</span> <span class="nx">databasePassword</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Add the import as well:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">bcrypt</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'bcryptjs'</span><span class="p">);</span>
</code></pre></div></div>
<p>This helper can now be used when we pull a user from the database and check that the passwords are equal:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">passport</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="k">new</span> <span class="nx">LocalStrategy</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">(</span><span class="nx">username</span><span class="p">,</span> <span class="nx">password</span><span class="p">,</span> <span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'users'</span><span class="p">).</span><span class="nx">where</span><span class="p">({</span> <span class="nx">username</span> <span class="p">}).</span><span class="nx">first</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">user</span><span class="p">)</span> <span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">comparePass</span><span class="p">(</span><span class="nx">password</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">password</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">user</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="k">return</span> <span class="nx">done</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
<span class="p">}));</span>
</code></pre></div></div>
<p>Next, update the <code class="highlighter-rouge">addUser</code> function in <em>src/server/db/queries/users.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">bcrypt</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'bcryptjs'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">knex</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../connection'</span><span class="p">);</span>
<span class="kd">function</span> <span class="nx">addUser</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">salt</span> <span class="o">=</span> <span class="nx">bcrypt</span><span class="p">.</span><span class="nx">genSaltSync</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">hash</span> <span class="o">=</span> <span class="nx">bcrypt</span><span class="p">.</span><span class="nx">hashSync</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">password</span><span class="p">,</span> <span class="nx">salt</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'users'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">insert</span><span class="p">({</span>
<span class="na">username</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">username</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="nx">hash</span><span class="p">,</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">returning</span><span class="p">(</span><span class="s1">'*'</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">addUser</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Do the same for the user seed in <em>src/server/db/seeds/users.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">bcrypt</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'bcryptjs'</span><span class="p">);</span>
<span class="nx">exports</span><span class="p">.</span><span class="nx">seed</span> <span class="o">=</span> <span class="p">(</span><span class="nx">knex</span><span class="p">,</span> <span class="nb">Promise</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">salt</span> <span class="o">=</span> <span class="nx">bcrypt</span><span class="p">.</span><span class="nx">genSaltSync</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">hash</span> <span class="o">=</span> <span class="nx">bcrypt</span><span class="p">.</span><span class="nx">hashSync</span><span class="p">(</span><span class="s1">'johnson'</span><span class="p">,</span> <span class="nx">salt</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'users'</span><span class="p">).</span><span class="nx">del</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'users'</span><span class="p">).</span><span class="nx">insert</span><span class="p">({</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'jeremy'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="nx">hash</span><span class="p">,</span>
<span class="p">})</span>
<span class="p">);</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Now, instead of adding a plain text password to the database, we salt and hash it first.</p>
<p>Drop and recreate the <code class="highlighter-rouge">koa_api</code> database, apply the migrations, and then run the server and manually test everything out.</p>
<p>Finally, make sure the tests still pass:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm test
Server listening on port: 1337
routes : auth
GET /auth/register
✓ should render the register view
POST /auth/register
✓ should register a new user (211ms)
GET /auth/login
✓ should render the login view
POST /auth/login
✓ should login a user (99ms)
routes : index
GET /
✓ should return json
routes : movies
GET /api/v1/movies
✓ should return all movies
GET /api/v1/movies/:id
✓ should respond with a single movie
✓ should throw an error if the movie does not exist
POST /api/v1/movies
✓ should return the movie that was added
✓ should throw an error if the payload is malformed
PUT /api/v1/movies
✓ should return the movie that was updated
✓ should throw an error if the movie does not exist
DELETE /api/v1/movies/:id
✓ should return the movie that was deleted
✓ should throw an error if the movie does not exist
Sample Test
✓ should pass
15 passing (3s)
</code></pre></div></div>
<h2 id="redis-session-store">Redis Session Store</h2>
<p>It’s a good idea to move session data out of memory and into an external session store as you begin scaling your application.</p>
<p>For example, if you scale horizontally and start spinning up new instances of the same Node application to share the load, then users would need to log in to each instance separately if sessions are stored in memory. On the other hand, if sessions are stored in an external session store (like Redis), session data can be shared across all instances of the app. In the latter case, users would need to log in just once.</p>
<p>To utilize Redis as the session store, first install <a href="https://github.com/koajs/koa-redis">koa-redis</a>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm install koa-redis@3.1.1 --save
</code></pre></div></div>
<p>Then, update the <code class="highlighter-rouge">koa-session</code> middleware config in <em>src/server/index.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">session</span><span class="p">({</span>
<span class="na">store</span><span class="p">:</span> <span class="k">new</span> <span class="nx">RedisStore</span><span class="p">()</span>
<span class="p">},</span> <span class="nx">app</span><span class="p">));</span>
</code></pre></div></div>
<p>Add the dependency:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">RedisStore</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-redis'</span><span class="p">);</span>
</code></pre></div></div>
<p>Take note of the <a href="https://github.com/koajs/koa-redis#options">default options</a> for koa-redis, making any necessary changes. Then, <a href="https://redis.io/download">download and install Redis</a> (if necessary) and spin up the server in a new terminal tab:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>redis-server
</code></pre></div></div>
<p>Fire up the app and register a new user, taking note of the cookie:</p>
<p><img src="/assets/img/blog/koa/koa-passport-cookie.png" alt="koa passport cookie" /></p>
<p>Within another new terminal tab, open the Redis client and make sure that key can be found:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>redis-cli
127.0.0.1:6379> keys 1nmcdC3apKbGVOk-VfbKjMR1dcgDUH1S
1<span class="o">)</span> <span class="s2">"1nmcdC3apKbGVOk-VfbKjMR1dcgDUH1S"</span>
127.0.0.1:6379> <span class="nb">exit</span>
</code></pre></div></div>
<p>Run the tests one final time.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this tutorial, we went through the process of adding authentication to a Koa app with Passport. Turn back to the objectives. Review each one. What did you learn?</p>
<p>The full code can be found in the <a href="https://github.com/mjhea0/node-koa-api/releases/tag/v3">v3</a> tag of the <a href="https://github.com/mjhea0/node-koa-api">node-koa-api</a> repository.</p>
<p>Check your understanding by adding additional test cases and error handlers, if you have not already done so. Add the missing tests to the <code class="highlighter-rouge">/auth/status</code> and <code class="highlighter-rouge">/auth/logout</code> routes by stubbing out the authentication functionality. Refer to the <a href="http://mherman.org/blog/2017/11/06/stubbing-http-requests-with-sinon">Stubbing HTTP Requests with Sinon</a> blog post for help. Try adding social authentication too!</p>
<p>Please share your comments, questions, and/or tips in the comments below.</p>
Michael Herman
Passport is a library that provides a simple authentication middleware for Node.js.
Docker on AWS: from containerization to orchestration
2017-11-16T00:00:00-06:00
2017-11-16T00:00:00-06:00
https://mherman.org/blog/docker-on-aws-from-containerization-to-orchestration
<p>In this post, we’ll take a number of containerized microservices running on a single EC2 instance and scale them out to Amazon’s container orchestration service, <a href="https://aws.amazon.com/ecs/">EC2 Container Service</a> (ECS).</p>
<div style="text-align:center;">
<img src="/assets/img/blog/docker-aws/aws-docker.png" style="max-width: 100%; border:0; box-shadow: none;" alt="aws and docker" />
</div>
<p><br /></p>
<p>We’ll be using the following tools…</p>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Use Cases</th>
<th>Version</th>
</tr>
</thead>
<tbody>
<tr>
<td>Docker</td>
<td>Containerization and distribution</td>
<td><a href="https://docs.docker.com/release-notes/docker-ce/#17090-ce-2017-09-26">17.09.0-ce</a></td>
</tr>
<tr>
<td>AWS ECS</td>
<td>Container orchestration and management</td>
<td><a href="https://github.com/aws/amazon-ecs-agent/releases/tag/v1.15.0">1.15.0</a> (service agent)</td>
</tr>
</tbody>
</table>
<p><em>Updates:</em></p>
<ul>
<li>Nov 25, 2017: Updated the Inbound Rules on the Security Group.</li>
</ul>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#prerequisites" id="markdown-toc-prerequisites">Prerequisites</a></li>
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#containerization" id="markdown-toc-containerization">Containerization</a></li>
<li><a href="#orchestration" id="markdown-toc-orchestration">Orchestration</a></li>
<li><a href="#next-steps" id="markdown-toc-next-steps">Next Steps</a></li>
</ul>
<h2 id="prerequisites">Prerequisites</h2>
<p>This post assumes prior knowledge of Docker and some experience working with a Docker-based microservice stack. You should also be familiar with the following AWS services - <a href="https://aws.amazon.com/vpc/">VPC</a>, <a href="https://aws.amazon.com/elasticloadbalancing/">ELB</a>, <a href="https://aws.amazon.com/ec2/">EC2</a>, and <a href="https://aws.amazon.com/iam/">IAM</a>.</p>
<p>Refer to the following resources for more info:</p>
<ol>
<li><a href="http://mherman.org/blog/2017/05/11/developing-microservices-node-react-docker">Developing Microservices - Node, React, and Docker</a></li>
<li><a href="https://testdriven.io">Microservices with Docker, Flask, and React</a> - full course, powered by the fine folks at <a href="https://testdriven.io">testdriven.io</a>!</li>
</ol>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you will be able to…</p>
<ol>
<li>Explain what <em>container orchestration</em> is and why you may need to incorporate an orchestration tool into your deployment process</li>
<li>Discuss the pros and cons of using EC2 Container Service (ECS) over other <em>orchestration tools</em> like Kubernetes, Mesos, and Docker Swarm</li>
<li>Configure an <em>Application Load Balancer</em> (ALB) along with <em>ECS</em> to run a set of microservices</li>
<li>Integrate <em>Amazon EC2 Container Registry</em> (ECR) into the deployment process</li>
<li>Send <em>container logs</em> to CloudWatch</li>
<li>Update a running container via a <em>zero-downtime deployment</em> strategy to not disrupt the current users or your application</li>
<li>Explain the types of <em>scaling</em> that are available to you</li>
</ol>
<h2 id="project-setup">Project Setup</h2>
<p>Fork the <a href="https://github.com/mjhea0/microservice-ping-pong">microservice-ping-pong</a> repo, clone it down, and then check out the <a href="https://github.com/mjhea0/microservice-ping-pong/releases/tag/v1">v1</a> tag to the master branch:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/YOUR_GITHUB_NAME/microservice-ping-pong
<span class="nv">$ </span><span class="nb">cd </span>microservice-ping-pong
<span class="nv">$ </span>git checkout tags/v1 <span class="nt">-b</span> master
</code></pre></div></div>
<p>Take note of the <em>docker-compose.yml</em> file to see an overview of the project structure:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.3'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">node-john</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">node-john</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">./services/node</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">node-base</span>
<span class="s">...</span>
<span class="na">node-paul</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">node-paul</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">node-base</span>
<span class="s">...</span>
<span class="na">node-george</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">node-george</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">node-base</span>
<span class="s">...</span>
<span class="na">node-ringo</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">node-ringo</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">node-base</span>
<span class="s">...</span>
<span class="na">client</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">node-client</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">./services/client</span>
<span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span>
<span class="s">...</span>
<span class="na">nginx</span><span class="pi">:</span>
<span class="na">container_name</span><span class="pi">:</span> <span class="s">node-nginx</span>
<span class="na">build</span><span class="pi">:</span> <span class="s">./services/nginx</span>
<span class="s">...</span>
</code></pre></div></div>
<p>So, that’s one Nginx container and one React container as well as four Node containers. Also, did you notice that we’re using the same image to build each of the Node containers?</p>
<div style="text-align:left;">
<table style="margin:0;">
<thead>
<tr>
<th> Container </th>
<th> Image </th>
<th> Tech </th>
</tr>
</thead>
<tbody>
<tr>
<td> node-john </td>
<td> node-base </td>
<td> NodeJs </td>
</tr>
<tr>
<td> node-paul </td>
<td> node-base </td>
<td> NodeJS </td>
</tr>
<tr>
<td> node-george </td>
<td> node-base </td>
<td> NodeJS </td>
</tr>
<tr>
<td> node-ringo </td>
<td> node-base </td>
<td> NodeJS </td>
</tr>
<tr>
<td> client </td>
<td> client </td>
<td> ReactJS </td>
</tr>
<tr>
<td> nginx </td>
<td> nginx </td>
<td> Nginx </td>
</tr>
</tbody>
</table>
</div>
<p></p>
<p>Feel free to fire up the app locally:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker build <span class="nt">-t</span> node-base ./services/node
<span class="nv">$ </span><span class="nb">export </span><span class="nv">REACT_APP_BASE_URL</span><span class="o">=</span>http://DOCKER_MACHINE_IP
<span class="nv">$ </span>docker-compose up <span class="nt">-d</span> <span class="nt">--build</span>
</code></pre></div></div>
<p><a href="/assets/img/blog/docker-aws/ping.png"><img src="/assets/img/blog/docker-aws/ping.png" alt="ping pong app" /></a></p>
<p>The functionality is quite simple: When the end user clicks on one of the letters, an AJAX request is sent to the <code class="highlighter-rouge">node-john</code> container, which then triggers a series of container-to-container requests:</p>
<ol>
<li><code class="highlighter-rouge">node-john</code> requests <code class="highlighter-rouge">node-paul</code></li>
<li><code class="highlighter-rouge">node-paul</code> requests <code class="highlighter-rouge">node-george</code></li>
<li><code class="highlighter-rouge">node-george</code> requests <code class="highlighter-rouge">node-ringo</code></li>
</ol>
<p>Once complete, a response is sent back to the client with the following array:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
</span><span class="s2">"meow from node-paul:3000"</span><span class="p">,</span><span class="w">
</span><span class="s2">"meow from node-george:3000"</span><span class="p">,</span><span class="w">
</span><span class="s2">"meow from node-ringo:3000"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>Nothing special. Just a basic app, meant to highlight some aspects of how networking works in Docker land.</p>
<h2 id="containerization">Containerization</h2>
<p>Next, let’s deploy the cluster to a single Amazon EC2 instance with Docker <a href="https://docs.docker.com/compose/">Compose</a> and <a href="https://docs.docker.com/machine/">Machine</a>.</p>
<h3 id="docker-machine">Docker Machine</h3>
<p>Assuming you already have an AWS account <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/get-set-up-for-amazon-ec2.html">setup</a> along with <a href="https://aws.amazon.com/iam/">IAM</a> and your AWS credentials are stored in an <em>~/.aws/credentials</em> file, create a new host on an EC2 instance:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-machine create <span class="nt">--driver</span> amazonec2 ping-pong
</code></pre></div></div>
<blockquote>
<p>For more, review the <a href="https://docs.docker.com/machine/examples/aws/">Amazon Web Services (AWS) EC2 example</a> from Docker.</p>
</blockquote>
<p>Once done, set it as the active host and point the Docker client at it:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-machine env ping-pong
<span class="nv">$ </span><span class="nb">eval</span> <span class="k">$(</span>docker-machine env ping-pong<span class="k">)</span>
</code></pre></div></div>
<p>Grab the IP address associated with the new EC2 instance and use it to set the <code class="highlighter-rouge">REACT_APP_BASE_URL</code> environment variable:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-machine ip ping-pong
<span class="nv">$ </span><span class="nb">export </span><span class="nv">REACT_APP_BASE_URL</span><span class="o">=</span>http://DOCKER_MACHINE_IP
</code></pre></div></div>
<blockquote>
<p>The <code class="highlighter-rouge">REACT_APP_BASE_URL</code> environment variable must be set at the build-time, so it is available <em>before</em> we kick off Create React App’s production build process. See <em>services/client/Dockerfile</em> for more info.</p>
</blockquote>
<p>Build and tag the <code class="highlighter-rouge">node-base</code> image:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker build <span class="nt">-t</span> node-base ./services/node
</code></pre></div></div>
<p>Fire up the containers:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">-d</span> <span class="nt">--build</span>
</code></pre></div></div>
<p>Make sure to expose port 80 in the <a href="https://stackoverflow.com/questions/26338301/ec2-how-to-add-port-8080-in-security-group">Security Group</a>, and then test it out in the browser:</p>
<div>
<img src="/assets/img/blog/docker-aws/ping3.png" style="max-width: 80%; border:0; box-shadow: none;" alt="ping pong app" />
</div>
<p>Back in your terminal, open the Docker <a href="https://docs.docker.com/compose/reference/logs/">logs</a> with a <a href="https://docs.docker.com/compose/reference/logs/">follow</a> flag:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose logs <span class="nt">-f</span>
</code></pre></div></div>
<p>Then, with your terminal window and browser side-by-side on your screen, click on of the letters:</p>
<p><a href="/assets/img/blog/docker-aws/ping2.png"><img src="/assets/img/blog/docker-aws/ping2.png" alt="ping pong app" /></a></p>
<p>You should see a number of POST requests in the terminal, as the containers ping each other.</p>
<p>It’s important to note that the majority of your apps can live on a single instance like this. Spin up Postgres on <a href="https://aws.amazon.com/rds/">RDS</a> and possibly a message queue on <a href="https://aws.amazon.com/sqs/">SQS</a> and you should be good for a while. As your app grows and you add feature after feature, you may that you need to create separate services and scale them independency from the whole. At that point, it may be time to start looking at ECS.</p>
<p>With that, let’s look at how scale this out to multiple EC2 instances with <a href="https://aws.amazon.com/ecs/">ECS</a>…</p>
<h2 id="orchestration">Orchestration</h2>
<blockquote>
<p>If you’re completely new to ECS, please review the <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_GetStarted.html">Getting Started with Amazon ECS</a> guide.</p>
</blockquote>
<h3 id="what-is-container-orchestration">What is container orchestration?</h3>
<p>As you move from deploying containers on a single machine to deploying them across a number of machines, you need an orchestration tool to manage the arrangement and coordination of the containers across the entire system. This is where ECS fits in along with a number of other orchestration tools - like <a href="https://kubernetes.io/">Kubernetes</a>, <a href="http://mesos.apache.org/">Mesos</a>, and <a href="https://docs.docker.com/engine/swarm/">Docker Swarm</a>.</p>
<div>
<img src="/assets/img/blog/docker-aws/kubernetes-vs-docker-swarm-vs-mesos.png" style="max-width: 40%; border:0; box-shadow: none;" alt="kubernetes vs docker swarm vs mesos" />
</div>
<p></p>
<h3 id="why-ecs">Why ECS?</h3>
<p>ECS is simpler to set up and easier to use and you have the full power of AWS behind it, so you can easily integrate it into other AWS services (which we will be doing shortly). In short, you get scheduling, service discovery, load balancing, and auto-scaling out-of-the-box. Plus, you can take full advantage of EC2’s multiple availability-zones.</p>
<p>If you’re already on AWS and have no desire to leave, then it makes sense to use AWS.</p>
<p>Keep in mind, that ECS is often lagging behind Kubernetes, in terms of features, though. If you’re looking for the most features and portability and you don’t mind installing and managing the tool, then Kubernetes, Docker Swarm, or Mesos may be right for you.</p>
<p>One last thing to take note of is that since ECS is closed-source, there isn’t a true way to run an environment locally in order to achieve development-to-production parity.</p>
<blockquote>
<p>For more, review the <a href="https://blog.kublr.com/choosing-the-right-containerization-and-cluster-management-tool-fdfcec5700df">Choosing the Right Containerization and Cluster Management Tool</a> blog post.</p>
</blockquote>
<h3 id="orchestration-feature-wish-list">Orchestration feature wish-list</h3>
<p>Most orchestration tools come with a core set of features. You can find those features below along with the associated AWS service…</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Info</th>
<th>AWS Service</th>
</tr>
</thead>
<tbody>
<tr>
<td>Health checks</td>
<td>Verify when a task is ready to accept traffic</td>
<td>ALB</td>
</tr>
<tr>
<td>Path-based routing</td>
<td>Forward requests based on the URL path</td>
<td>ALB</td>
</tr>
<tr>
<td>Dynamic port-mapping</td>
<td>Ports are assigned dynamically when a new container is spun up</td>
<td>ALB</td>
</tr>
<tr>
<td>Zero-downtime deployments</td>
<td>Deployments do not disrupt the users</td>
<td>ALB</td>
</tr>
<tr>
<td>Service discovery</td>
<td>Automatic detection of new containers and services</td>
<td>ALB, ECS</td>
</tr>
<tr>
<td>High availability</td>
<td>Containers are evenly distributed across Availability Zones</td>
<td>ECS</td>
</tr>
<tr>
<td>Auto scaling</td>
<td>Automatically scaling resources up or down based on fluctuations in traffic patterns or metrics (like CPU usage)</td>
<td>ECS</td>
</tr>
<tr>
<td>Provisioning</td>
<td>New containers should select hosts based on resources and configuration</td>
<td>ECS</td>
</tr>
<tr>
<td>Container storage</td>
<td>Private image storage and management</td>
<td>ECR</td>
</tr>
<tr>
<td>Container logs</td>
<td>Centralized storage of container logs</td>
<td>CloudWatch</td>
</tr>
<tr>
<td>Monitoring</td>
<td>Ability to monitor basic stats like CPU usage, memory, I/O, and network usage as well as set alarms and create events</td>
<td>CloudWatch</td>
</tr>
<tr>
<td>Secrets management</td>
<td>Sensitive info should be encrypted and stored in a centralized store</td>
<td>Parameter Store, KMS, IAM</td>
</tr>
</tbody>
</table>
<h3 id="elastic-load-balancer">Elastic Load Balancer</h3>
<p>The <a href="https://aws.amazon.com/elasticloadbalancing/">Elastic Load Balancer</a> distributes incoming application traffic and scales resources as needed to meet traffic needs.</p>
<p>A load balancer is one of (if not) the most important parts of your applications since it needs to always be up, routing traffic to healthy back-ends, and ready to scale at a moment’s notice.</p>
<p>There are currently <a href="https://aws.amazon.com/elasticloadbalancing/details/#details">three types</a> of Elastic Load Balancers to choose from. We’ll be using the Application Load Balancer since it provides support for <a href="http://docs.aws.amazon.com/elasticloadbalancing/latest/application/tutorial-load-balancer-routing.html">path-based routing</a> and <a href="https://aws.amazon.com/premiumsupport/knowledge-center/dynamic-port-mapping-ecs/">dynamic port-mapping</a> and it also enables zero-downtime deployments. The Application Load Balancer is one of those AWS services that makes ECS so powerful. In fact, before it’s <a href="https://aws.amazon.com/blogs/aws/new-aws-application-load-balancer/">release</a>, ECS was not a viable orchestration solution.</p>
<p>To set up, click “Load Balancers” in the <a href="https://console.aws.amazon.com/ec2">EC2 Dashboard</a>. Click “Create Load Balancer”, and then select the “Create” button under “Application Load Balancer”.</p>
<h4 id="configure-load-balancer">Configure Load Balancer</h4>
<ul>
<li>“Name”: <code class="highlighter-rouge">microservice-ping-pong-alb</code></li>
<li>“VPC”: Select the <a href="http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/default-vpc.html">default VPC</a> to keep things simple</li>
<li>“Availability Zones”: Select at least two available subnets</li>
</ul>
<p><a href="/assets/img/blog/docker-aws/configure-load-balancer1.png"><img src="/assets/img/blog/docker-aws/configure-load-balancer1.png" alt="configure load balancer" /></a></p>
<h4 id="configure-security-settings">Configure Security Settings</h4>
<p>Skip this for now.</p>
<h4 id="configure-security-groups">Configure Security Groups</h4>
<p>Create a new <a href="http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_SecurityGroups.html">Security Group</a> called <code class="highlighter-rouge">microservice-ping-pong-security-group</code>, making sure to open up port 80.</p>
<p><a href="/assets/img/blog/docker-aws/configure-load-balancer2.png"><img src="/assets/img/blog/docker-aws/configure-load-balancer2.png" alt="configure load balancer" /></a></p>
<h4 id="configure-routing">Configure Routing</h4>
<ul>
<li>“Name”: <code class="highlighter-rouge">microservice-ping-pong-client-tg</code></li>
<li>“Port”: <code class="highlighter-rouge">3000</code></li>
<li>“Path”: /</li>
</ul>
<p><a href="/assets/img/blog/docker-aws/configure-load-balancer3.png"><img src="/assets/img/blog/docker-aws/configure-load-balancer3.png" alt="configure load balancer" /></a></p>
<h4 id="register-targets">Register Targets</h4>
<p>Do not assign any instances manually since this will be managed by ECS. Review and then create the new load balancer.</p>
<p>Once created, take note of the new Security Group:</p>
<p><a href="/assets/img/blog/docker-aws/load-balancer.png"><img src="/assets/img/blog/docker-aws/load-balancer.png" alt="aws ecs load balancer" /></a></p>
<p>With that, we also need to set up Target Groups and Listeners:</p>
<p><a href="/assets/img/blog/docker-aws/elastic-load-balancing.png"><img src="/assets/img/blog/docker-aws/elastic-load-balancing.png" alt="elastic load balancing " /></a></p>
<h3 id="target-groups">Target Groups</h3>
<p><a href="http://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html">Target Groups</a> are attached to the Application Load Balancer and are used to route traffic to the containers found in the ECS Service.</p>
<p>You may have already noticed, but a Target Group called <code class="highlighter-rouge">microservice-ping-pong-client-tg</code> was already created when we set up the Application Load Balancer, so we just need to set up one more.</p>
<p>Within the <a href="https://console.aws.amazon.com/ec2">EC2 Dashboard</a>, click “Target Groups”, and then create the following Target Group:</p>
<ul>
<li>“Target group name”: <code class="highlighter-rouge">microservice-ping-pong-node-tg</code></li>
<li>“Port”: <code class="highlighter-rouge">3000</code></li>
<li>Then, under “Health check settings” set the “Path” to <code class="highlighter-rouge">/ping</code>.</li>
</ul>
<p><a href="/assets/img/blog/docker-aws/configure-target-group.png"><img src="/assets/img/blog/docker-aws/configure-target-group.png" alt="configure target groups" /></a></p>
<h3 id="listeners">Listeners</h3>
<p>Back on the “Load Balancers” page on the <a href="https://console.aws.amazon.com/ec2">EC2 Dashboard</a>, click the <code class="highlighter-rouge">microservice-ping-pong-alb</code> Load Balancer, and then select the “Listeners” tab. Here, we can add <a href="http://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html">Listeners</a> to the load balancer, which are then forwarded to a specific Target Group.</p>
<p>There should already be a listener for “HTTP : 80”. Click the “View/edit rules >” link, and then insert three new rules:</p>
<ol>
<li>If <code class="highlighter-rouge">/</code>, Then <code class="highlighter-rouge">microservice-ping-pong-client-tg</code></li>
<li>If <code class="highlighter-rouge">/ping</code>, Then <code class="highlighter-rouge">microservice-ping-pong-client-tg</code></li>
<li>If <code class="highlighter-rouge">/start</code>, Then <code class="highlighter-rouge">microservice-ping-pong-client-tg</code></li>
</ol>
<p><a href="/assets/img/blog/docker-aws/load-balancer-listeners.png"><img src="/assets/img/blog/docker-aws/load-balancer-listeners.png" alt="aws ecr" /></a></p>
<h3 id="ecr">ECR</h3>
<p>Next, we’ll set up <a href="https://aws.amazon.com/ecr/">EC2 Container Registry</a> (ECR), a private image registry.</p>
<p>Navigate to the <a href="https://console.aws.amazon.com/ecs">ECS Console</a>, click “Repositories” on the navigation pane, and then click the “Create repository” button. Add the following repositories, making sure to follow the build, tag, and push commands after each is created:</p>
<ol>
<li><code class="highlighter-rouge">microservice-ping-pong/client</code></li>
<li><code class="highlighter-rouge">microservice-ping-pong/node</code></li>
</ol>
<blockquote>
<p>Be sure to update the value of the <code class="highlighter-rouge">REACT_APP_BASE_URL</code> environment variable to the Load Balancer’s “DNS name”.</p>
</blockquote>
<p>Example build, tag, and push:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>aws ecr get-login <span class="nt">--no-include-email</span> <span class="nt">--region</span> us-east-1
<span class="nv">$ </span>docker build <span class="nt">-t</span> microservice-ping-pong/client ./services/client
<span class="nv">$ </span>docker tag microservice-ping-pong/client:latest <span class="se">\</span>
AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/microservice-ping-pong/client:latest
<span class="nv">$ </span>docker push <span class="se">\</span>
AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/microservice-ping-pong/client:latest
</code></pre></div></div>
<h4 id="why-only-two-images">Why only two images?</h4>
<p>Well, we’ll use the Application Load Balancer instead of Nginx in our stack so we won’t need that image or container. For the Node containers, just like before, we can use one image to create all four containers since they are identical.</p>
<p><a href="/assets/img/blog/docker-aws/ecr.png"><img src="/assets/img/blog/docker-aws/ecr.png" alt="aws ecr" /></a></p>
<p>Did you notice that we tagged the image with <code class="highlighter-rouge">latest</code> in the example above? This is an anti-pattern. Tags can (and should) be used for version control as well as denoting which environment the image should belong to - like development, pre-prod/staging, or production.</p>
<p>For example, you could use both the commit git commit SHA1 hash (to associate the image back to a specific commit to help with debugging) along with and the environment name.</p>
<p><code class="highlighter-rouge">/$PROJECT/$ENVIRONMENT:$SHA1</code></p>
<h3 id="ecs">ECS</h3>
<p>The <a href="https://aws.amazon.com/ecs/">EC2 Container Service</a> (ECS) has four main components:</p>
<ol>
<li>Task Definitions</li>
<li>Tasks</li>
<li>Services</li>
<li>Clusters</li>
</ol>
<p>In short, Task Definitions are used to spin up Tasks that get assigned to a Service, which is then assigned to a Cluster.</p>
<p><img src="/assets/img/blog/docker-aws/ecs.png" alt="ecs" /></p>
<h4 id="clusters">Clusters</h4>
<p>An <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_clusters.html">ECS Cluster</a> is just a group of EC2 container instances managed by ECS.</p>
<p>To create a Cluster, click “Clusters” on the <a href="https://console.aws.amazon.com/ecs">ECS Console</a> sidebar, and then click the “Create Cluster” button.</p>
<ol>
<li>“Cluster name”: <code class="highlighter-rouge">microservice-ping-pong-cluster</code></li>
<li>“EC2 instance type”: <code class="highlighter-rouge">t2.small</code></li>
<li>“Number of instances”: <code class="highlighter-rouge">2</code></li>
<li>“Key pair”: Select an existing <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html">Key Pair</a> or create a new one</li>
<li>Select the default VPC and the previously created Security Group along with the appropriate subnets</li>
</ol>
<p><a href="/assets/img/blog/docker-aws/configure-cluster.png"><img src="/assets/img/blog/docker-aws/configure-cluster.png" alt="configure cluster" /></a></p>
<p>Navigate to the Cluster once it’s up, and then click the “ECS Instances” tab. From there, click the “Actions” dropdown and select “View Cluster Resources” to ensure all is well:</p>
<p><a href="/assets/img/blog/docker-aws/cluster.png"><img src="/assets/img/blog/docker-aws/cluster.png" alt="aws ecs cluster" /></a></p>
<h4 id="task-definitions">Task Definitions</h4>
<p><a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html">Task Definitions</a> define which containers make up the overall application and how much resources are allocated to each container. You can think of them as blueprints.</p>
<p>Within the <a href="https://console.aws.amazon.com/ecs">ECS Console</a>, click “Task Definitions” and then “Create new Task Definition”.</p>
<p>First, Update the “Task Definition Name” to <code class="highlighter-rouge">microservice-ping-pong-client-td</code> and then add a new container:</p>
<ul>
<li>“Container name”: <code class="highlighter-rouge">client</code></li>
<li>“Image”: <code class="highlighter-rouge">AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/microservice-ping-pong/client:latest</code></li>
<li>“Memory Limits (MB)”: <code class="highlighter-rouge">300</code> soft limit</li>
<li>“Port mappings”: <code class="highlighter-rouge">0</code> host, <code class="highlighter-rouge">3000</code> container</li>
</ul>
<blockquote>
<p>We set the host port for the client service to 0 so that a port is dynamically assigned when the Task is spun up.</p>
</blockquote>
<ul>
<li>“Log configuration”: It’s a good idea to configure logs, via <a href="http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_LogConfiguration.html">LogConfiguration</a>, to pipe logs to <a href="https://console.aws.amazon.com/cloudwatch/">CloudWatch</a>. To set up, we need to create a new Log Group. Simply navigate to <a href="https://console.aws.amazon.com/cloudwatch">CloudWatch</a>, click “Logs” on the navigation pane, click the “Actions” drop-down button, and then select “Create log group”. Name the group <code class="highlighter-rouge">microservice-ping-pong-client</code>.</li>
</ul>
<p><a href="/assets/img/blog/docker-aws/configure-task-def.png"><img src="/assets/img/blog/docker-aws/configure-task-def.png" alt="configure task def" /></a></p>
<p><a href="/assets/img/blog/docker-aws/configure-task-def2.png"><img src="/assets/img/blog/docker-aws/configure-task-def2.png" alt="configure task def" /></a></p>
<p>Then, set up a single Task Definition for each of the Node containers:</p>
<ul>
<li>“Name”: <code class="highlighter-rouge">microservice-ping-pong-node-td</code></li>
<li>“Container”:
<ul>
<li>“Container name”: <code class="highlighter-rouge">node-john</code></li>
<li>“Image”: <code class="highlighter-rouge">AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/microservice-ping-pong/node:latest</code></li>
<li>“Memory Limits (MB)”: <code class="highlighter-rouge">300</code> soft limit</li>
<li>“Port mappings”: <code class="highlighter-rouge">0</code> host, <code class="highlighter-rouge">3000</code> container</li>
<li>“Links”: <code class="highlighter-rouge">node-paul</code>, <code class="highlighter-rouge">node-george</code>, <code class="highlighter-rouge">node-ringo</code></li>
<li>“Log configuration”: <code class="highlighter-rouge">microservice-ping-pong-node</code></li>
</ul>
</li>
<li>“Container”:
<ul>
<li>“Container name”: <code class="highlighter-rouge">node-paul</code></li>
<li>“Image”: <code class="highlighter-rouge">AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/microservice-ping-pong/node:latest</code></li>
<li>“Memory Limits (MB)”: <code class="highlighter-rouge">300</code> soft limit</li>
<li>“Port mappings”: <code class="highlighter-rouge">0</code> host, <code class="highlighter-rouge">3000</code> container</li>
<li>“Log configuration”: <code class="highlighter-rouge">microservice-ping-pong-node</code></li>
</ul>
</li>
<li>“Container”:
<ul>
<li>“Container name”: <code class="highlighter-rouge">node-george</code></li>
<li>“Image”: <code class="highlighter-rouge">AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/microservice-ping-pong/node:latest</code></li>
<li>“Memory Limits (MB)”: <code class="highlighter-rouge">300</code> soft limit</li>
<li>“Port mappings”: <code class="highlighter-rouge">0</code> host, <code class="highlighter-rouge">3000</code> container</li>
<li>“Log configuration”: <code class="highlighter-rouge">microservice-ping-pong-node</code></li>
</ul>
</li>
<li>“Container”:
<ul>
<li>“Container name”: <code class="highlighter-rouge">node-ringo</code></li>
<li>“Image”: <code class="highlighter-rouge">AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/microservice-ping-pong/node:latest</code></li>
<li>“Memory Limits (MB)”: <code class="highlighter-rouge">300</code> soft limit</li>
<li>“Port mappings”: <code class="highlighter-rouge">0</code> host, <code class="highlighter-rouge">3000</code> container</li>
<li>“Log configuration”: <code class="highlighter-rouge">microservice-ping-pong-node</code></li>
</ul>
</li>
</ul>
<p><a href="/assets/img/blog/docker-aws/task-defs.png"><img src="/assets/img/blog/docker-aws/task-defs.png" alt="aws ecs task definitions" /></a></p>
<h4 id="services">Services</h4>
<p><a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html">Services</a> instantiate the containers from the Task Definitions and run them on EC2 boxes within an ECS Cluster. Such instances are called <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_run_task.html">Tasks</a>. To create a new Service, on the “Services” tab within the newly created Cluster, click “Create”.</p>
<p>Add the following Services…</p>
<h5 id="client">Client</h5>
<ul>
<li>“Task Definition”: <code class="highlighter-rouge">microservice-ping-pong-client-td:LATEST_REVISION_NUMBER</code></li>
<li>“Service name”: <code class="highlighter-rouge">microservice-ping-pong-client-service</code></li>
<li>“Number of tasks”: 1</li>
</ul>
<p><a href="/assets/img/blog/docker-aws/configure-service1.png"><img src="/assets/img/blog/docker-aws/configure-service1.png" alt="configure service" /></a></p>
<p>Click “Next”. On the “Configure network” step, select the “Application Load Balancer” under “Load balancer type”.</p>
<ul>
<li>“Load balancer name”: <code class="highlighter-rouge">microservice-ping-pong-alb</code></li>
<li>“Container name : port”: <code class="highlighter-rouge">client:0:3000</code></li>
</ul>
<p><a href="/assets/img/blog/docker-aws/configure-service2.png"><img src="/assets/img/blog/docker-aws/configure-service2.png" alt="configure service" /></a></p>
<p>Click “Add to load balancer”.</p>
<ul>
<li>“Listener port”: <code class="highlighter-rouge">80:HTTP</code></li>
<li>“Target group name”: <code class="highlighter-rouge">microservice-ping-pong-client-tg</code></li>
</ul>
<p><a href="/assets/img/blog/docker-aws/configure-service3.png"><img src="/assets/img/blog/docker-aws/configure-service3.png" alt="configure service" /></a></p>
<p>Click the next button a few times, and then “Create Service”.</p>
<h5 id="node">Node</h5>
<ul>
<li>“Task Definition”: <code class="highlighter-rouge">microservice-ping-pong-node-td:LATEST_REVISION_NUMBER</code></li>
<li>“Service name”: <code class="highlighter-rouge">microservice-ping-pong-node-service</code></li>
<li>“Number of tasks”: 1</li>
</ul>
<p>Click “Next”. On the “Configure network” step, select the “Application Load Balancer” under “Load balancer type”.</p>
<ul>
<li>“Load balancer name”: <code class="highlighter-rouge">microservice-ping-pong-alb</code></li>
<li>“Container name : port”: <code class="highlighter-rouge">node-john:0:3000</code></li>
</ul>
<p>Click “Add to load balancer”.</p>
<ul>
<li>“Listener port”: <code class="highlighter-rouge">80:HTTP</code></li>
<li>“Target group name”: <code class="highlighter-rouge">microservice-ping-pong-node-tg</code></li>
</ul>
<p><a href="/assets/img/blog/docker-aws/ecs-services.png"><img src="/assets/img/blog/docker-aws/ecs-services.png" alt="aws ecs services" /></a></p>
<h3 id="sanity-check">Sanity Check</h3>
<p>Navigate to the <a href="https://console.aws.amazon.com/ec2">EC2 Dashboard</a>, and click “Target Groups”. Make sure <code class="highlighter-rouge">microservice-ping-pong-client-tg</code> and <code class="highlighter-rouge">microservice-ping-pong-node-tg</code> have a single registered instance each. Both instances should also be <em>unhealthy</em> because they failed their respective health checks.</p>
<p>To get them to pass the health checks, we need to add another inbound rule to the Security Group associated with the containers (which we defined when we configured the Cluster), <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/create-application-load-balancer.html#alb-sec-group">allowing</a> traffic from the Load Balancer to reach the containers.</p>
<h3 id="inbound-rule">Inbound Rule</h3>
<p>Within the <a href="https://console.aws.amazon.com/ec2">EC2 Dashboard</a>, click “Security Groups” and select the Security group associated with the containers, which is the same group assigned to the Load Balancer). Click the “Inbound” tab and then click “Edit”</p>
<p>Add a new rule:</p>
<ol>
<li>“Type”: <code class="highlighter-rouge">All traffic</code></li>
<li>“Port Range”: <code class="highlighter-rouge">0 - 65535</code></li>
<li>“Source”: Choose <code class="highlighter-rouge">Custom</code>, then add the Security Group ID</li>
</ol>
<p>Once added, the next time a container is added to each of the Target Groups, the instance should be <em>healthy</em>:</p>
<p><a href="/assets/img/blog/docker-aws/target-groups.png"><img src="/assets/img/blog/docker-aws/target-groups.png" alt="aws target groups" /></a></p>
<p>Then, navigate back to the Load Balancer and grab the “DNS name” from the “Description” tab. Test each endpoint in your browser:</p>
<ol>
<li><a href="http://LOAD_BALANCER_DNS_NAME">http://LOAD_BALANCER_DNS_NAME</a></li>
<li><a href="http://LOAD_BALANCER_DNS_NAME/ping">http://LOAD_BALANCER_DNS_NAME/ping</a></li>
<li><a href="http://LOAD_BALANCER_DNS_NAME/start">http://LOAD_BALANCER_DNS_NAME/start</a></li>
</ol>
<p>Essentially, when the Service was spun up, ECS automatically discovered and associated the new Cluster instances with the Application Load Balancer.</p>
<p><a href="/assets/img/blog/docker-aws/elastic-load-balancing-ecs.png"><img src="/assets/img/blog/docker-aws/elastic-load-balancing-ecs.png" alt="elastic load balancing and ecs " /></a></p>
<h3 id="zero-downtime-deployments">Zero Downtime Deployments</h3>
<p>Check you understanding and try this on your own.</p>
<h4 id="steps">Steps</h4>
<ol>
<li>Make a quick change to the app locally.</li>
<li>Build, tag, and push the new images.</li>
<li>
<p>Add a new revision to the task definition.</p>
<p><a href="/assets/img/blog/docker-aws/task-def-revision.png"><img src="/assets/img/blog/docker-aws/task-def-revision.png" alt="task definition revision " /></a></p>
</li>
<li>
<p>Update the service.</p>
<p><a href="/assets/img/blog/docker-aws/service-update1.png"><img src="/assets/img/blog/docker-aws/service-update1.png" alt="update service " /></a></p>
<p><a href="/assets/img/blog/docker-aws/service-update2.png"><img src="/assets/img/blog/docker-aws/service-update2.png" alt="update service " /></a></p>
</li>
</ol>
<h4 id="what-happens-next">What happens next?</h4>
<ol>
<li>
<p>Once you update the Service, ECS will pick up on these changes and instantiate the Task Definitions, creating new Tasks that will spin up on the Cluster instances.</p>
<p><a href="/assets/img/blog/docker-aws/service-update3.png"><img src="/assets/img/blog/docker-aws/service-update3.png" alt="update service " /></a></p>
</li>
<li>
<p>ALB will run health checks on the new instances once they are up.</p>
<ul>
<li>
<p>If the health checks pass, traffic is forwarded appropriately to the new Tasks while the old Tasks are spun down.</p>
<p><a href="/assets/img/blog/docker-aws/service-update4.png"><img src="/assets/img/blog/docker-aws/service-update4.png" alt="update service " /></a></p>
</li>
<li>
<p>If the health checks fail, the new Tasks are spun down.</p>
</li>
</ul>
</li>
</ol>
<p>The health checks are the last line of defense after your own unit, integration, and functional tests.</p>
<div>
<img src="/assets/img/blog/docker-aws/ping4.png" style="max-width: 80%; border:0; box-shadow: none;" alt="ping pong app" />
</div>
<h3 id="autoscaling">Autoscaling</h3>
<p>You can scale up or down at both the Cluster (adding additional EC2 instances) and Service (adding more Tasks to an existing instance) level.</p>
<h4 id="cluster">Cluster</h4>
<p>To manually scale, navigate to the Cluster and click the “ECS Instances” tab. Then, click the “Scale ECS Instances” button and provide the desired number of instances you’d like to scale up (or down) to.</p>
<p><a href="/assets/img/blog/docker-aws/scale-by-cluster.png"><img src="/assets/img/blog/docker-aws/scale-by-cluster.png" alt="scale by cluster " /></a></p>
<p>You can automate this process by setting up an <a href="http://docs.aws.amazon.com/autoscaling/latest/userguide/AutoScalingGroup.html">Auto Scaling Group</a>. Review the <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/cloudwatch_alarm_autoscaling.html">Scaling Container Instances with CloudWatch Alarms</a> tutorial for more info.</p>
<h4 id="service">Service</h4>
<p>You can also scale Tasks up (or down) at the Service-level.</p>
<p><a href="/assets/img/blog/docker-aws/service-auto-scaling.png"><img src="/assets/img/blog/docker-aws/service-auto-scaling.png" alt="service auto scaling " /></a></p>
<p><a href="/assets/img/blog/docker-aws/service-auto-scaling2.png"><img src="/assets/img/blog/docker-aws/service-auto-scaling2.png" alt="service auto scaling " /></a></p>
<p>For more on this, review the <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/service_autoscaling_tutorial.html">Service Auto Scaling with CloudWatch Service Utilization Metrics</a> tutorial.</p>
<h2 id="next-steps">Next Steps</h2>
<p>That’s it!</p>
<h3 id="check-your-understanding">Check your understanding</h3>
<ol>
<li>Add CI/CD (via <a href="http://mherman.org/blog/2017/09/18/on-demand-test-environments-with-docker-and-aws-ecs">Circle CI</a> or <a href="https://medium.com/@YadavPrakshi/automate-zero-downtime-deployment-with-amazon-ecs-and-lambda-c4e49953273d">AWS Lambda</a>) and Postgres via RDS (<a href="https://testdriven.io">example</a>)</li>
<li>Turn back to the feature wish-list. Implement anything not covered.</li>
<li>Did you notice that we didn’t add any of the environment variables from the Docker Compose file to the Task Definitions? Why does the app still work? Update the Task Definitions on your own.</li>
</ol>
<h3 id="resources">Resources</h3>
<ol>
<li><a href="http://mherman.org/presentations/microservice-ping-pong">Slides</a></li>
<li><a href="https://github.com/mjhea0/microservice-ping-pong">Repo</a></li>
<li><strong><em><a href="http://testdriven.io/">Testdriven.io</a> - full tutorial!</em></strong> ❤️</li>
<li><a href="https://hub.packtpub.com/how-to-build-12-factor-design-microservices-on-docker-part-1/">How to Build 12 Factor Microservices on Docker</a></li>
<li><a href="https://github.com/wsargent/docker-cheat-sheet">Docker Cheat Sheet</a></li>
</ol>
<p>Cheers!</p>
Michael Herman
In this post, we’ll take a number of containerized microservices running on a single EC2 instance and scale them out to Amazon’s container orchestration service, EC2 Container Service (ECS).
Stubbing HTTP Requests with Sinon
2017-11-06T00:00:00-06:00
2017-11-06T00:00:00-06:00
https://mherman.org/blog/stubbing-http-requests-with-sinon
<p>This tutorial details how to stub HTTP requests with <a href="http://sinonjs.org/">Sinon.js</a> during test runs.</p>
<div style="text-align:center;">
<img src="/assets/img/blog/sinonjs.png" style="max-width: 90%; border:0; box-shadow: none;" alt="sinon.js" />
</div>
<p>If you’re developing for the web, you are most likely connecting to some other external service to extend the functionality of your application. You could be connecting to a third-party API - like Twilio, GitHub, Twitter, or Mailgun, to name a few - or just communicating with another service in your microservice stack. Regardless, when unit testing, you do not want HTTP requests to go out to these services. Instead, you can “fake” the request and response with a stub, tricking the system into thinking the request was made.</p>
<h4 id="parts">Parts</h4>
<p>This article is part of a 4-part Koa and Sinon series…</p>
<ol>
<li><a href="http://mherman.org/blog/2017/08/23/building-a-restful-api-with-koa-and-postgres">Building a RESTful API with Koa and Postgres</a></li>
<li><a href="http://mherman.org/blog/2017/11/06/stubbing-http-requests-with-sinon">Stubbing HTTP Requests with Sinon</a> (this article)</li>
<li><a href="http://mherman.org/blog/2018/01/02/user-authentication-with-passport-and-koa">User Authentication with Passport and Koa</a></li>
<li><a href="http://mherman.org/blog/2018/01/22/stubbing-node-authentication-middleware-with-sinon">Stubbing Node Authentication Middleware with Sinon</a></li>
</ol>
<h4 id="npm-dependencies">NPM Dependencies</h4>
<ol>
<li>Node v<a href="https://nodejs.org/en/blog/release/v8.7.0/">8.7.0</a></li>
<li>Mocha v<a href="https://github.com/mochajs/mocha/releases/tag/v4.0.1">4.0.1</a></li>
<li>Chai v<a href="https://github.com/chaijs/chai/releases/tag/4.1.2">4.1.2</a></li>
<li>Sinon v<a href="http://sinonjs.org/releases/v4.1.1/">4.1.1</a></li>
<li>Request v<a href="https://github.com/request/request/releases/tag/v2.83.0">2.83.0</a></li>
</ol>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#what-is-a-stub" id="markdown-toc-what-is-a-stub">What is a Stub?</a></li>
<li><a href="#why-stub" id="markdown-toc-why-stub">Why Stub?</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#sinon-setup" id="markdown-toc-sinon-setup">Sinon Setup</a></li>
<li><a href="#testing-the-movie-service" id="markdown-toc-testing-the-movie-service">Testing the Movie Service</a></li>
<li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you will be able to…</p>
<ol>
<li>Describe what a stub is and why you would want to use them in your test suites</li>
<li>Discuss the benefits of using Sinon to stub calls to external services</li>
<li>Set up a testing structure with Mocha, Chai, and Sinon</li>
<li>Write full integration tests to call an external service during the test run</li>
<li>Refactor integration tests to unit tests, stubbing out the external HTTP requests</li>
<li>Stub each of the CRUD functions from an external service</li>
</ol>
<h2 id="what-is-a-stub">What is a Stub?</h2>
<p>In testing land, a stub replaces real behavior with a fixed version. In the case of HTTP requests, instead of making the actual call, a stub fakes the call and provides a canned response that can be used to test against.</p>
<p>It’s important to note that you should not rely solely on fake data in place of real data when testing. At some point in the testing process, possibly in a staging/pre-prod environment, you should test out all external communication so that you can be confident that the system works as expected. This is often achieved with some form of end-to-end tests.</p>
<p>Also, keep in mind, that it can be quite difficult to keep the testing behavior aligned with the actual behavior of the service. It’s common for this to happen when a service is updated and the stub stays the same. Because of this, you should limit your use of stubs to <a href="https://en.wikipedia.org/wiki/Input/output">I/O</a> operations and processes that are CPU intensive.</p>
<blockquote>
<p>For more on stubs and fakes, check out the excellent <a href="https://martinfowler.com/articles/mocksArentStubs.html">Mocks Aren’t Stubs</a> article.</p>
</blockquote>
<h2 id="why-stub">Why Stub?</h2>
<p>Calling external services during test runs can cause a number of problems:</p>
<ol>
<li>First off, this will slow down your test suite. Calling external services, especially in a microservice stack, can result in a ping-pong affect with HTTP requests and responses.</li>
<li>Tests will often fail due to network outages and other connectivity issues, like the service being down.</li>
<li>Often, third-party services have rate limits in place. Getting around this can be tricky (creating new test accounts on the fly) or costly (upgrading to the next service tier).</li>
<li>The service itself may not have a staging or sandbox mode for testing. In this case, you would actually be testing a service in production, so extra care needs to be taken to prevent test data from polluting production data.</li>
<li>Finally, the service itself may not be fully implemented or it may not even exist yet, which is common in a microservice stack.</li>
</ol>
<p><em>Isolating tests by stubbing external service calls makes testing faster, simpler, and more predictable.</em> Then, once you have some data to play with, you can use it in other parts of your test suite - to test a front-end UI, for example.</p>
<h2 id="project-setup">Project Setup</h2>
<p>Let’s start by spinning up the external service that we’ll consume from for testing purposes in the integration tests.</p>
<h3 id="movie-service">Movie Service</h3>
<p>Clone down the <a href="https://github.com/mjhea0/node-koa-api">project</a>, check out the <a href="https://github.com/mjhea0/node-koa-api/releases/tag/v2">v2</a> tag to the master branch, and install the dependencies:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/mjhea0/node-koa-api <span class="se">\</span>
<span class="nt">--branch</span> v2 <span class="nt">--single-branch</span>
<span class="nv">$ </span><span class="nb">cd </span>node-koa-api
<span class="nv">$ </span>git checkout tags/v2 <span class="nt">-b</span> master
<span class="nv">$ </span>npm install
</code></pre></div></div>
<p>Take a quick look at the code. This is just a simple Node RESTful API, with the following routes:</p>
<table>
<thead>
<tr>
<th>URL</th>
<th>HTTP Verb</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>/api/v1/movies</td>
<td>GET</td>
<td>Return ALL movies</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>GET</td>
<td>Return a SINGLE movie</td>
</tr>
<tr>
<td>/api/v1/movies</td>
<td>POST</td>
<td>Add a movie</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>PUT</td>
<td>Update a movie</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>DELETE</td>
<td>Delete a movie</td>
</tr>
</tbody>
</table>
<blockquote>
<p>Want to learn how to build this project? Check out the <a href="http://mherman.org/blog/2017/08/23/building-a-restful-api-with-koa-and-postgres">Building a RESTful API With Koa and Postgres</a> blog post.</p>
</blockquote>
<p>With Postgres up and running on port 5432, open psql in the terminal, and create the databases:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>psql
psql <span class="o">(</span>9.6.1<span class="o">)</span>
<span class="c"># CREATE DATABASE koa_api;</span>
CREATE DATABASE
<span class="c"># CREATE DATABASE koa_api_test;</span>
CREATE DATABASE
<span class="c"># \q</span>
</code></pre></div></div>
<p>Apply the migrations and seed the database:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knex migrate:latest <span class="nt">--env</span> development
<span class="nv">$ </span>knex seed:run <span class="nt">--env</span> development
</code></pre></div></div>
<p>Fire up the service:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm start
</code></pre></div></div>
<p>Then, navigate to <a href="http://localhost:1337/api/v1/movies">http://localhost:1337/api/v1/movies</a> in your favorite browser, and you should see all the movies:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w">
</span><span class="s2">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The Land Before Time"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Fantasy"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">7</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Jurassic Park"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Science Fiction"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">6</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Ice Age: Dawn of the Dinosaurs"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Action/Romance"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>With that, let’s set up the testing framework boilerplate.</p>
<h3 id="mocha-and-chai">Mocha and Chai</h3>
<p>Clone down the base project:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/mjhea0/mocha-chai-sinon <span class="se">\</span>
<span class="nt">--branch</span> v1 <span class="nt">--single-branch</span>
<span class="nv">$ </span><span class="nb">cd </span>mocha-chai-sinon
</code></pre></div></div>
<p>Then, check out the v1 tag to the master branch and install the dependencies:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git checkout tags/v1 <span class="nt">-b</span> master
<span class="nv">$ </span>npm install
</code></pre></div></div>
<p>Make sure the tests pass:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm <span class="nb">test
</span>Sample Test
✓ should pass
1 passing <span class="o">(</span>8ms<span class="o">)</span>
</code></pre></div></div>
<p>Take a quick look at the project structure before moving on.</p>
<h2 id="sinon-setup">Sinon Setup</h2>
<p>Install:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install sinon@4.1.1 <span class="nt">--save-dev</span>
</code></pre></div></div>
<blockquote>
<p>While that’s installing, do some basic research on the libraries available to stub (or mock) HTTP requests in Node. How does Sinon compare to these other libraries?</p>
</blockquote>
<p>Let’s start with a basic example. Add the following code to <em>test/sample.test.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">greaterThanTwenty</span><span class="p">(</span><span class="nx">num</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">num</span> <span class="o">></span> <span class="mi">20</span><span class="p">)</span> <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
<span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'Sample Sinon Stub'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should pass'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">greaterThanTwenty</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">().</span><span class="nx">returns</span><span class="p">(</span><span class="s1">'something'</span><span class="p">);</span>
<span class="nx">greaterThanTwenty</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'something'</span><span class="p">);</span>
<span class="nx">greaterThanTwenty</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Here, we stubbed out the <code class="highlighter-rouge">greaterThanTwenty</code> function, overriding the function’s default behavior, so that it returns <code class="highlighter-rouge">'something'</code> instead of either <code class="highlighter-rouge">true</code> or <code class="highlighter-rouge">false</code>.</p>
<p>Run the tests to ensure they pass:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sample Test
✓ should pass
Sample Sinon Stub
✓ should pass
</code></pre></div></div>
<p>You can also stub a prototype method:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">Person</span><span class="p">(</span><span class="nx">givenName</span><span class="p">,</span> <span class="nx">familyName</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">givenName</span> <span class="o">=</span> <span class="nx">givenName</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">familyName</span> <span class="o">=</span> <span class="nx">familyName</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">Person</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">getFullName</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">givenName</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">familyName</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
<span class="p">};</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'Sample Sinon Stub Take 2'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should pass'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">name</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Person</span><span class="p">(</span><span class="s1">'Michael'</span><span class="p">,</span> <span class="s1">'Herman'</span><span class="p">);</span>
<span class="nx">name</span><span class="p">.</span><span class="nx">getFullName</span><span class="p">().</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'Michael Herman'</span><span class="p">);</span>
<span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">Person</span><span class="p">.</span><span class="nx">prototype</span><span class="p">,</span> <span class="s1">'getFullName'</span><span class="p">).</span><span class="nx">returns</span><span class="p">(</span><span class="s1">'John Doe'</span><span class="p">);</span>
<span class="nx">name</span><span class="p">.</span><span class="nx">getFullName</span><span class="p">().</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'John Doe'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Again, make sure the tests pass:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sample Test
✓ should pass
Sample Sinon Stub
✓ should pass
Sample Sinon Stub Take 2
✓ should pass
</code></pre></div></div>
<blockquote>
<p>For more examples, review the official <a href="http://sinonjs.org/releases/v4.0.1/stubs/">docs</a>.</p>
</blockquote>
<p>With that, let’s now look at stubbing HTTP requests.</p>
<h2 id="testing-the-movie-service">Testing the Movie Service</h2>
<p>Create a new file in “test” called <em>movie.service.test.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">=</span> <span class="s1">'test'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">sinon</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'sinon'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'request'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">chai</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'chai'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">should</span> <span class="o">=</span> <span class="nx">chai</span><span class="p">.</span><span class="nx">should</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">base</span> <span class="o">=</span> <span class="s1">'http://localhost:1337'</span><span class="p">;</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'movie service'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'when not stubbed'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// test cases</span>
<span class="p">});</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'when stubbed'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="kd">get</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="s1">'get'</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">request</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
<span class="c1">// test cases</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>So, we’ll test out the movie service using both unit and integrations tests so you can see the difference. Take note of the <code class="highlighter-rouge">beforeEach</code> and <code class="highlighter-rouge">afterEach</code> functions. Here, we stubbed the <code class="highlighter-rouge">get</code> method, from the <code class="highlighter-rouge">request</code> package (assigning it to <code class="highlighter-rouge">this.get</code> so we can reference it later in the test cases), in the <code class="highlighter-rouge">beforeEach()</code>, and then we restored the original behavior in the <code class="highlighter-rouge">afterEach()</code>.</p>
<p>Install the <a href="https://github.com/request/request">Request</a> library:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install request@2.83.0 <span class="nt">--save-dev</span>
</code></pre></div></div>
<h3 id="get-all-movies">GET All Movies</h3>
<p>Start with the integration test:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /api/v1/movies'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should return all movies'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">request</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies`</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// there should be a 200 status code</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="c1">// the response should be JSON</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="c1">// parse response body</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"status": "success"}</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"data": [3 movie objects]}</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">length</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
<span class="c1">// the first object in the data array should</span>
<span class="c1">// have the right keys</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="c1">// the first object should have the right value for name</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">name</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'The Land Before Time'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Take note of the code comments. This should be fairly straightforward. With the movie service up and running, ensure the test passes:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>movie service
when not stubbed
GET /api/v1/movies
✓ should <span class="k">return </span>all movies
</code></pre></div></div>
<p>Now, let’s look at how to stub the HTTP request call. Update the “when stubbed” <code class="highlighter-rouge">describe</code> block, like so:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'when stubbed'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">responseObject</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">statusCode</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'content-type'</span><span class="p">:</span> <span class="s1">'application/json'</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">responseBody</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">id</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'The Land Before Time'</span><span class="p">,</span>
<span class="na">genre</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,</span>
<span class="na">rating</span><span class="p">:</span> <span class="mi">7</span><span class="p">,</span>
<span class="na">explicit</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">id</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'Jurassic Park'</span><span class="p">,</span>
<span class="na">genre</span><span class="p">:</span> <span class="s1">'Science Fiction'</span><span class="p">,</span>
<span class="na">rating</span><span class="p">:</span> <span class="mi">9</span><span class="p">,</span>
<span class="na">explicit</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">id</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'Ice Age: Dawn of the Dinosaurs'</span><span class="p">,</span>
<span class="na">genre</span><span class="p">:</span> <span class="s1">'Action/Romance'</span><span class="p">,</span>
<span class="na">rating</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
<span class="na">explicit</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">};</span>
<span class="k">this</span><span class="p">.</span><span class="kd">get</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="s1">'get'</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">request</span><span class="p">.</span><span class="kd">get</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /api/v1/movies'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should return all movies'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="kd">get</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">responseObject</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">responseBody</span><span class="p">));</span>
<span class="nx">request</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies`</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// there should be a 200 status code</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="c1">// the response should be JSON</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="c1">// parse response body</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"status": "success"}</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"data": [3 movie objects]}</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">length</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
<span class="c1">// the first object in the data array should</span>
<span class="c1">// have the right keys</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="c1">// the first object should have the right value for name</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">name</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'The Land Before Time'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Here, we use the <a href="http://sinonjs.org/releases/v1.17.7/stubs/">yields</a> method to automatically call the callback passed to <code class="highlighter-rouge">get()</code>. Remember how <code class="highlighter-rouge">request</code> works? After the request is sent, the function waits until the callback is called before proceeding. So, by stubbing this out, we simply pass in a dummy object and immediately call the callback.</p>
<p>Make sure the tests pass:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>movie service
when not stubbed
GET /api/v1/movies
✓ should <span class="k">return </span>all movies <span class="o">(</span>47ms<span class="o">)</span>
when stubbed
GET /api/v1/movies
✓ should <span class="k">return </span>all movies
</code></pre></div></div>
<p>Now, how do we know the call wasn’t actually made?</p>
<ol>
<li>Kill the movie service server</li>
<li>Add a <code class="highlighter-rouge">.only</code> to the <code class="highlighter-rouge">describe</code> block - <code class="highlighter-rouge">describe.only('when stubbed', () => {</code></li>
</ol>
<p>Test again. It should still pass. Once done, remove the <code class="highlighter-rouge">.only</code> and fire the movie service back up.</p>
<h3 id="fixture">Fixture</h3>
<p>To keep things clean in the test cases and to make it easy to find and update the fake objects, let’s create a test fixtures file.</p>
<p>Add a new folder to “test” called “fixtures”, and then add a new file to that folder called <em>movies.json</em>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"all"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"success"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"res"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"statusCode"</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span><span class="p">,</span><span class="w">
</span><span class="s2">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"content-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"body"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w">
</span><span class="s2">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The Land Before Time"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Fantasy"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">7</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Jurassic Park"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Science Fiction"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">6</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Ice Age: Dawn of the Dinosaurs"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Action/Romance"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Import the file into <em>movies.service.test.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">movies</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./fixtures/movies.json'</span><span class="p">);</span>
</code></pre></div></div>
<p>Update <code class="highlighter-rouge">this.get.yields</code>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="p">.</span><span class="kd">get</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span>
<span class="kc">null</span><span class="p">,</span> <span class="nx">movies</span><span class="p">.</span><span class="nx">all</span><span class="p">.</span><span class="nx">success</span><span class="p">.</span><span class="nx">res</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">movies</span><span class="p">.</span><span class="nx">all</span><span class="p">.</span><span class="nx">success</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>
<p>Make sure the tests still pass!</p>
<blockquote>
<p>You may also want to add the expected data for the assertions to the fixtures as well. Or: You could take it a few steps further and generate the actual test cases from the fixture file. Try this on your own.</p>
</blockquote>
<h3 id="get-single-movie">GET Single Movie</h3>
<h4 id="integration-test">Integration test</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /api/v1/movies/:id'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should respond with a single movie'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">request</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies/4`</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">name</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'The Land Before Time'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should throw an error if the movie does not exist'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">request</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies/999`</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">404</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'error'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">message</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'That movie does not exist.'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Notice how we used the movie id of <code class="highlighter-rouge">4</code> in the first test. This makes for a brittle test, since it will fail if that movie is removed or the name is updated in the movie service. Sure, we do have control over the data in this service, but in most cases you will not have this luxury.</p>
<h4 id="unit-test">Unit test</h4>
<p>Add the fixture to the <em>movies.json</em> file:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"single"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"success"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"res"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"statusCode"</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span><span class="p">,</span><span class="w">
</span><span class="s2">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"content-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"body"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w">
</span><span class="s2">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The Land Before Time"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Fantasy"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">7</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"failure"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"res"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"statusCode"</span><span class="p">:</span><span class="w"> </span><span class="mi">404</span><span class="p">,</span><span class="w">
</span><span class="s2">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"content-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"body"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"error"</span><span class="p">,</span><span class="w">
</span><span class="s2">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"That movie does not exist."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Then, add the test cases:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /api/v1/movies/:id'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should respond with a single movie'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">obj</span> <span class="o">=</span> <span class="nx">movies</span><span class="p">.</span><span class="nx">single</span><span class="p">.</span><span class="nx">success</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="kd">get</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">res</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">obj</span><span class="p">.</span><span class="nx">body</span><span class="p">));</span>
<span class="nx">request</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies/4`</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">name</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'The Land Before Time'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should throw an error if the movie does not exist'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">obj</span> <span class="o">=</span> <span class="nx">movies</span><span class="p">.</span><span class="nx">single</span><span class="p">.</span><span class="nx">failure</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="kd">get</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">res</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">obj</span><span class="p">.</span><span class="nx">body</span><span class="p">));</span>
<span class="nx">request</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies/999`</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">404</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'error'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">message</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'That movie does not exist.'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Run the tests:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>movie service
when not stubbed
GET /api/v1/movies
✓ should return all movies (40ms)
GET /api/v1/movies/:id
✓ should respond with a single movie
✓ should throw an error if the movie does not exist
when stubbed
GET /api/v1/movies
✓ should return all movies
GET /api/v1/movies/:id
✓ should respond with a single movie
✓ should throw an error if the movie does not exist
</code></pre></div></div>
<h3 id="post">POST</h3>
<h4 id="integration-test-1">Integration test</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'POST /api/v1/movies'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should return the movie that was added'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="s1">'post'</span><span class="p">,</span>
<span class="na">body</span><span class="p">:</span> <span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'Titanic'</span><span class="p">,</span>
<span class="na">genre</span><span class="p">:</span> <span class="s1">'Drama'</span><span class="p">,</span>
<span class="na">rating</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
<span class="na">explicit</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">},</span>
<span class="na">json</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">url</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies`</span>
<span class="p">};</span>
<span class="nx">request</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">201</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Run the tests. They should pass the first time, but if you run them again, you should see the first test, <code class="highlighter-rouge">should return all movies</code>, fail:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Uncaught AssertionError: expected 4 to deeply equal 3
</code></pre></div></div>
<p>How can we fix this?</p>
<ol>
<li>Remove the assertion altogether from the first test case</li>
<li>Add a <code class="highlighter-rouge">beforeEach</code> that removes any test data that was added from a previously ran test case (this will add additional requests, slowing down the test suite even more)</li>
</ol>
<p>Either way, this is not an easy issue to fix, especially if it’s a third-party service that you have no control over. This is one of the reasons why we’re stubbing in the first place - to limit the complexity. So, instead of trying to fix the integration test, let’s just remove it and focus solely on the unit tests:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">.</span><span class="nx">skip</span><span class="p">(</span><span class="s1">'when not stubbed'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="p">})</span>
</code></pre></div></div>
<h4 id="unit-test-1">Unit Test</h4>
<p>Start with the fixture:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"add"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"success"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"res"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"statusCode"</span><span class="p">:</span><span class="w"> </span><span class="mi">201</span><span class="p">,</span><span class="w">
</span><span class="s2">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"content-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"body"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w">
</span><span class="s2">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Titanic"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Drama"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">8</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Next, stub the <code class="highlighter-rouge">post</code> method:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="kd">get</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="s1">'get'</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">post</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="s1">'post'</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">request</span><span class="p">.</span><span class="kd">get</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="nx">request</span><span class="p">.</span><span class="nx">post</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Add the test:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'POST /api/v1/movies'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should return the movie that was added'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">body</span><span class="p">:</span> <span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'Titanic'</span><span class="p">,</span>
<span class="na">genre</span><span class="p">:</span> <span class="s1">'Drama'</span><span class="p">,</span>
<span class="na">rating</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
<span class="na">explicit</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">},</span>
<span class="na">json</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">url</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies`</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">obj</span> <span class="o">=</span> <span class="nx">movies</span><span class="p">.</span><span class="nx">add</span><span class="p">.</span><span class="nx">success</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">post</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">res</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">obj</span><span class="p">.</span><span class="nx">body</span><span class="p">));</span>
<span class="nx">request</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">201</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">name</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'Titanic'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Make sure the tests pass:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>movie service
when stubbed
GET /api/v1/movies
✓ should return all movies
GET /api/v1/movies/:id
✓ should respond with a single movie
✓ should throw an error if the movie does not exist
POST /api/v1/movies
✓ should return the movie that was added
</code></pre></div></div>
<p>What if the payload does not include the correct keys? Update the fixture:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"failure"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"res"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"statusCode"</span><span class="p">:</span><span class="w"> </span><span class="mi">400</span><span class="p">,</span><span class="w">
</span><span class="s2">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"content-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"body"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"error"</span><span class="p">,</span><span class="w">
</span><span class="s2">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Something went wrong."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Add a new <code class="highlighter-rouge">it</code> block:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="s1">'should throw an error if the payload is malformed'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">body</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="s1">'Titanic'</span> <span class="p">},</span>
<span class="na">json</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">url</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies`</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">obj</span> <span class="o">=</span> <span class="nx">movies</span><span class="p">.</span><span class="nx">add</span><span class="p">.</span><span class="nx">failure</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">post</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">res</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">obj</span><span class="p">.</span><span class="nx">body</span><span class="p">));</span>
<span class="nx">request</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">400</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'error'</span><span class="p">);</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">body</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<h3 id="put">PUT</h3>
<p>Fixture:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"update"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"success"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"res"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"statusCode"</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span><span class="p">,</span><span class="w">
</span><span class="s2">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"content-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"body"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w">
</span><span class="s2">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Titanic"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Drama"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"failure"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"res"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"statusCode"</span><span class="p">:</span><span class="w"> </span><span class="mi">404</span><span class="p">,</span><span class="w">
</span><span class="s2">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"content-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"body"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"error"</span><span class="p">,</span><span class="w">
</span><span class="s2">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"That movie does not exist."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Stub:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="kd">get</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="s1">'get'</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">post</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="s1">'post'</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">put</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="s1">'put'</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">request</span><span class="p">.</span><span class="kd">get</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="nx">request</span><span class="p">.</span><span class="nx">post</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="nx">request</span><span class="p">.</span><span class="nx">put</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Tests:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'PUT /api/v1/movies'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should return the movie that was updated'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">body</span><span class="p">:</span> <span class="p">{</span> <span class="na">rating</span><span class="p">:</span> <span class="mi">9</span> <span class="p">},</span>
<span class="na">json</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">url</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies/5`</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">obj</span> <span class="o">=</span> <span class="nx">movies</span><span class="p">.</span><span class="nx">update</span><span class="p">.</span><span class="nx">success</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">put</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">res</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">obj</span><span class="p">.</span><span class="nx">body</span><span class="p">));</span>
<span class="nx">request</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">name</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'Titanic'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">rating</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">9</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should throw an error if the movie does not exist'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">body</span><span class="p">:</span> <span class="p">{</span> <span class="na">rating</span><span class="p">:</span> <span class="mi">9</span> <span class="p">},</span>
<span class="na">json</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">url</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies/5`</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">obj</span> <span class="o">=</span> <span class="nx">movies</span><span class="p">.</span><span class="nx">update</span><span class="p">.</span><span class="nx">failure</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">put</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">res</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">obj</span><span class="p">.</span><span class="nx">body</span><span class="p">));</span>
<span class="nx">request</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="nx">options</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">404</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'error'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">message</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'That movie does not exist.'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<h3 id="delete">DELETE</h3>
<p>Fixture:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"delete"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"success"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"res"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"statusCode"</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span><span class="p">,</span><span class="w">
</span><span class="s2">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"content-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"body"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w">
</span><span class="s2">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="s2">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Titanic"</span><span class="p">,</span><span class="w">
</span><span class="s2">"genre"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Drama"</span><span class="p">,</span><span class="w">
</span><span class="s2">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span><span class="p">,</span><span class="w">
</span><span class="s2">"explicit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"failure"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"res"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"statusCode"</span><span class="p">:</span><span class="w"> </span><span class="mi">404</span><span class="p">,</span><span class="w">
</span><span class="s2">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"content-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="s2">"body"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"error"</span><span class="p">,</span><span class="w">
</span><span class="s2">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"That movie does not exist."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Stub:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="kd">get</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="s1">'get'</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">post</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="s1">'post'</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">put</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="s1">'put'</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="k">delete</span> <span class="o">=</span> <span class="nx">sinon</span><span class="p">.</span><span class="nx">stub</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="s1">'delete'</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">request</span><span class="p">.</span><span class="kd">get</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="nx">request</span><span class="p">.</span><span class="nx">post</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="nx">request</span><span class="p">.</span><span class="nx">put</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="nx">request</span><span class="p">.</span><span class="k">delete</span><span class="p">.</span><span class="nx">restore</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Test:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'DELETE /api/v1/movies/:id'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should return the movie that was deleted'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">obj</span> <span class="o">=</span> <span class="nx">movies</span><span class="p">.</span><span class="k">delete</span><span class="p">.</span><span class="nx">success</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="k">delete</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">res</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">obj</span><span class="p">.</span><span class="nx">body</span><span class="p">));</span>
<span class="nx">request</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies/5`</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">name</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'Titanic'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should throw an error if the movie does not exist'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">obj</span> <span class="o">=</span> <span class="nx">movies</span><span class="p">.</span><span class="k">delete</span><span class="p">.</span><span class="nx">failure</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="k">delete</span><span class="p">.</span><span class="nx">yields</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">res</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">obj</span><span class="p">.</span><span class="nx">body</span><span class="p">));</span>
<span class="nx">request</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">base</span><span class="p">}</span><span class="s2">/api/v1/movies/5`</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">body</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">404</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">'content-type'</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">contain</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'error'</span><span class="p">);</span>
<span class="nx">body</span><span class="p">.</span><span class="nx">message</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'That movie does not exist.'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Run the tests one final time to ensure they all pass:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>movie service
when stubbed
GET /api/v1/movies
✓ should return all movies
GET /api/v1/movies/:id
✓ should respond with a single movie
✓ should throw an error if the movie does not exist
POST /api/v1/movies
✓ should return the movie that was added
✓ should throw an error if the payload is malformed
PUT /api/v1/movies
✓ should return the movie that was updated
✓ should throw an error if the movie does not exist
DELETE /api/v1/movies/:id
✓ should return the movie that was deleted
✓ should throw an error if the movie does not exist
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>You should now have a better understanding of both the <em>why</em> and <em>how</em> in terms of stubbing with Sinon. Even though this post focused on HTTP requests, you can apply the same logic to other areas of your application like client-side AJAX requests, database queries, and Redis lookups, to name a few.</p>
<p>Turn back to the objectives. Read each aloud to yourself. Can you put each one into action?</p>
<p>Finally, it’s important to stub HTTP calls to external services to avoid flaky tests, speed up the overall test suite, and make testing more predictable. Be sure to balance your stubbed tests with end-to-end tests in a staging environment to ensure the system works as expected.</p>
<p>Grab the final code from the <a href="https://github.com/mjhea0/mocha-chai-sinon">mocha-chai-sinon</a> repo. Cheers!</p>
Michael Herman
This tutorial details how to stub HTTP requests with Sinon.js during test runs.
On-Demand Environments with Docker and AWS ECS
2017-09-18T00:00:00-05:00
2017-09-18T00:00:00-05:00
https://mherman.org/blog/on-demand-test-environments-with-docker-and-aws-ecs
<p>In this tutorial, we’ll look at how to spin up reproducible (and easily-destructible), on-demand test environments with <a href="http://docker.com/">Docker</a>, <a href="https://aws.amazon.com/ecs/">Amazon EC2 Container Service</a> (ECS), and <a href="https://circleci.com/">Circle CI</a> (for continuous integration and delivery).</p>
<div style="text-align:center;">
<img src="/assets/img/blog/on-demand-environments/on-demand-envs.png" style="max-width: 90%; border:0; box-shadow: none;" alt="on demand test environments" />
</div>
<p>We’ll be using the following tools…</p>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Use Cases</th>
<th>Version</th>
</tr>
</thead>
<tbody>
<tr>
<td>Docker</td>
<td>Containerization and distribution</td>
<td><a href="https://github.com/moby/moby/releases/tag/v17.03.2-ce">17.03.2-ce</a></td>
</tr>
<tr>
<td>AWS ECS</td>
<td>Container orchestration and management</td>
<td><a href="https://github.com/aws/amazon-ecs-agent/releases/tag/v1.14.4">1.14.4</a> (service agent)</td>
</tr>
<tr>
<td>Circle CI</td>
<td>Continuous integration</td>
<td><a href="https://circleci.com/docs/2.0/">2.0</a></td>
</tr>
<tr>
<td>AWS JavaScript SDK</td>
<td>Interacting with AWS</td>
<td><a href="https://github.com/aws/aws-sdk-js/releases/tag/v2.114.0">2.114.0</a></td>
</tr>
</tbody>
</table>
<p>For a demo, check out the following <a href="https://www.youtube.com/watch?v=O4jlWN3IVhE">video</a>.</p>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#prerequisites" id="markdown-toc-prerequisites">Prerequisites</a></li>
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#introduction" id="markdown-toc-introduction">Introduction</a></li>
<li><a href="#development-workflow" id="markdown-toc-development-workflow">Development Workflow</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#manual-aws-setup" id="markdown-toc-manual-aws-setup">Manual AWS Setup</a></li>
<li><a href="#circle-ci-setup" id="markdown-toc-circle-ci-setup">Circle CI Setup</a></li>
<li><a href="#aws-sdk-setup" id="markdown-toc-aws-sdk-setup">AWS SDK Setup</a></li>
<li><a href="#deployment-script" id="markdown-toc-deployment-script">Deployment Script</a></li>
<li><a href="#testing" id="markdown-toc-testing">Testing</a></li>
<li><a href="#teardown" id="markdown-toc-teardown">Teardown</a></li>
<li><a href="#conclusion-and-next-steps" id="markdown-toc-conclusion-and-next-steps">Conclusion and Next Steps</a></li>
</ul>
<h2 id="prerequisites">Prerequisites</h2>
<p>This post assumes prior knowledge of Docker and Docker Compose along with microservice architecture in general.</p>
<p>If you haven’t already, review the <a href="http://mherman.org/blog/2017/05/11/developing-microservices-node-react-docker">Developing Microservices - Node, React, and Docker</a> blog post. This tutorial utilizes a <em>slightly</em> modified version of the finished project from that post, so be sure to review the code from the <a href="https://github.com/mjhea0/microservice-movies/releases/tag/v3">v3</a> tag of the <a href="https://github.com/mjhea0/microservice-movies">microservice-movies</a> repository, taking note of each service:</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Service</th>
<th>Container</th>
<th>Tech</th>
</tr>
</thead>
<tbody>
<tr>
<td>Web</td>
<td>Web</td>
<td>web</td>
<td>React, React-Router</td>
</tr>
<tr>
<td>Movies API</td>
<td>Movies</td>
<td>movies</td>
<td>Node, Express</td>
</tr>
<tr>
<td>Movies DB</td>
<td>Movies</td>
<td>movies-db</td>
<td>Postgres</td>
</tr>
<tr>
<td>Swagger</td>
<td>Movies</td>
<td>swagger</td>
<td>Swagger UI</td>
</tr>
<tr>
<td>Users API</td>
<td>Users</td>
<td>users</td>
<td>Node, Express</td>
</tr>
<tr>
<td>Users DB</td>
<td>Users</td>
<td>users-db</td>
<td>Postgres</td>
</tr>
<tr>
<td>Functional Tests</td>
<td>Test</td>
<td>n/a</td>
<td>TestCafe</td>
</tr>
</tbody>
</table>
<p>You should also be familiar with AWS in general along with the following AWS services - <a href="https://aws.amazon.com/vpc/">VPC</a>, <a href="https://aws.amazon.com/elasticloadbalancing/">ELB</a>, <a href="https://aws.amazon.com/ec2/">EC2</a>, and <a href="https://aws.amazon.com/iam/">IAM</a>.</p>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you will be able to…</p>
<ol>
<li>Explain what on-demand environments are, why you would want to use them, and the overall development workflow</li>
<li>Discuss the benefits of using on-demand environments</li>
<li>Automate the building, configuring, deploying, and maintaining of on-demand environments on AWS</li>
<li>Configure an Application Load Balancer and ECS to run a set of microservices</li>
<li>Set up continuous integration and deployment to ECS via Circle CI</li>
<li>Integrate Amazon EC2 Container Registry, an image registry, into the continuous integration process</li>
<li>Create a teardown script to remove the environment once testing is complete</li>
</ol>
<h2 id="introduction">Introduction</h2>
<p>The end goal of on-demand test environments is to allow developers to quickly and easily spin up multiple, independent testing environments.</p>
<p>In other words, testing environments are spun up as needed during the development process - when code is checked-in, for example - and, then, when no longer needed, spun down just as quickly to free up resources and keep costs down.</p>
<p>With microservices, testing a single service can be difficult, time consuming, and expensive since you often have to spin up a plethora of services, especially if you have resource and/or config-heavy services. On-demand environments simplify this, making testing and trial easy, decreasing the feedback cycle between review and development.</p>
<p>Such environments can used for running integration and end-to-end tests, QA, UAT, troubleshooting, and basic trial.</p>
<blockquote>
<p><strong>NOTE:</strong> For more on the benefits of using reproducible, on-demand test environments, review the <a href="https://www.cognizant.com/whitepapers/the-business-case-for-on-demand-test-services-codex1342.pdf">The Business Case for On-Demand Test Services</a> whitepaper.</p>
</blockquote>
<h2 id="development-workflow">Development Workflow</h2>
<p>With on-demand environments, the development workflow looks like…</p>
<h3 id="1-local-development">(1) Local development</h3>
<ol>
<li>Create a new feature branch from the master branch</li>
<li>Make code changes</li>
<li>Commit and push code to GitHub</li>
</ol>
<h3 id="2-continuous-integration">(2) Continuous integration</h3>
<ol>
<li>Open a new PR against the development branch</li>
<li>A new build is then triggered on Circle CI</li>
<li>If the build passes, manually merge the PR</li>
<li>A new build is triggered again on Circle CI</li>
<li>If the build passes, deployment occurs…</li>
</ol>
<h3 id="3-deployment-on-aws-via-deployment-scripts">(3) Deployment on AWS via deployment scripts</h3>
<ol>
<li>Images are created, tagged, and pushed to ECR</li>
<li>Task Definitions are registered on ECS</li>
<li>Target Groups are created</li>
<li>A Listener is added to the load balancer and Rules are added</li>
<li>ECS Services are created</li>
</ol>
<h3 id="4-testing">(4) Testing</h3>
<ol>
<li>End-to-end tests</li>
<li>Acceptance tests</li>
<li>UAT</li>
</ol>
<h3 id="5-teardown">(5) Teardown</h3>
<ol>
<li>All AWS Resources are torn down</li>
</ol>
<h2 id="project-setup">Project Setup</h2>
<p>Fork the <a href="https://github.com/mjhea0/microservice-movies">microservice-movies</a> repo, clone it down, and then check out the <a href="https://github.com/mjhea0/microservice-movies/releases/tag/v3">v3</a> tag to a new branch called <code class="highlighter-rouge">aws-docker-on-demand</code>:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/YOUR_GITHUB_NAME/microservice-movies
<span class="nv">$ </span><span class="nb">cd </span>microservice-movies
<span class="nv">$ </span>git checkout tags/v3 <span class="nt">-b</span> aws-docker-on-demand
</code></pre></div></div>
<p>Set the environment variables:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">export </span><span class="nv">NODE_ENV</span><span class="o">=</span><span class="nb">test</span>
<span class="nv">$ </span><span class="nb">export </span><span class="nv">REACT_APP_USERS_SERVICE_URL</span><span class="o">=</span>http://localhost:3000
<span class="nv">$ </span><span class="nb">export </span><span class="nv">REACT_APP_MOVIES_SERVICE_URL</span><span class="o">=</span>http://localhost:3001
</code></pre></div></div>
<p>Build the images and fire up the containers:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose <span class="nt">-f</span> docker-compose-review.yml build
<span class="nv">$ </span>docker-compose <span class="nt">-f</span> docker-compose-review.yml up <span class="nt">-d</span>
</code></pre></div></div>
<p>Run the tests:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose <span class="nt">-f</span> docker-compose-review.yml <span class="se">\</span>
run users-service-review npm <span class="nb">test</span> <span class="se">\ </span><span class="o">&&</span>
docker-compose <span class="nt">-f</span> docker-compose-review.yml <span class="se">\</span>
run movies-service-review npm <span class="nb">test</span> <span class="se">\ </span><span class="o">&&</span>
testcafe firefox tests/<span class="k">**</span>/<span class="k">*</span>.js
</code></pre></div></div>
<p>Finally, ensure you can view the app in your browser at <a href="http://localhost:3007">http://localhost:3007</a>, and then try logging in with username <code class="highlighter-rouge">michael</code> and password <code class="highlighter-rouge">herman</code>.</p>
<h2 id="manual-aws-setup">Manual AWS Setup</h2>
<p>Before deploying, we need to configure the the following AWS resources:</p>
<ol>
<li><a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html">EC2 Key Pair</a></li>
<li><a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_clusters.html">ECS Cluster</a></li>
<li><a href="https://aws.amazon.com/ecr/">EC2 Container Registry</a> (ECR)</li>
<li><a href="https://aws.amazon.com/elasticloadbalancing/applicationloadbalancer/">Application Load Balancer</a> (ALB)</li>
<li><a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-network-security.html">Security Group</a></li>
</ol>
<p>Keep in mind that you <em>could</em> create each of these resources dynamically as well. Clusters can take a bit of time to fire up since EC2 instances need to be spun up though. By setting up the Cluster beforehand, EC2 instances are up and running, ready for Tasks to be added.</p>
<h3 id="ec2-key-pair">EC2 Key Pair</h3>
<p>Within the <a href="https://console.aws.amazon.com/ec2/">EC2 Dashboard</a>, click “Key Pairs” on the navigation pane, and then click the “Create Key Pair” button. Name the key <code class="highlighter-rouge">microservicemovies-review</code>. Save the file in a safe place - i.e., “~/.ssh”.</p>
<h3 id="ecs-cluster">ECS Cluster</h3>
<p>An <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_clusters.html">ECS Cluster</a> is just a group of EC2 container instances managed by ECS. To set up, navigate to the <a href="https://console.aws.amazon.com/ecs">ECS Console</a>, and then <a href="http://docs.aws.amazon.com/awsconsolehelpdocs/latest/gsg/getting-started.html#select-region">select</a> the region for the Cluster on the right-side of the nav bar.</p>
<blockquote>
<p><strong>NOTE</strong>: This tutorial uses the <code class="highlighter-rouge">US West (Oregon)</code> / <code class="highlighter-rouge">us-west-2</code> region. Feel free to use the region of your choice. For more info, review the <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html">Regions and Availability Zones</a> guide.</p>
</blockquote>
<p>Next, click the “Create Cluster” button:</p>
<ol>
<li>“Cluster name”: <code class="highlighter-rouge">microservicemovies-review</code></li>
<li>“EC2 instance type”: <code class="highlighter-rouge">t2.medium</code></li>
<li>“Number of instances”: <code class="highlighter-rouge">3</code></li>
<li>“Key pair”: <code class="highlighter-rouge">microservicemovies-review</code></li>
<li>Create a new <a href="https://aws.amazon.com/vpc/">VPC</a> and <a href="http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_SecurityGroups.html">Security Group</a></li>
</ol>
<p>Create the Cluster.</p>
<p>Navigate to the Cluster once it’s created, and then click the “ECS Instances” tab. From there, click the “Actions” dropdown and select “View Cluster Resources”. Take note of the VPC and Security Group:</p>
<p><a href="/assets/img/blog/on-demand-environments/aws-ecs-cluster-resources.png"><img src="/assets/img/blog/on-demand-environments/aws-ecs-cluster-resources.png" alt="aws ecs cluster resources" /></a></p>
<h3 id="ecr">ECR</h3>
<p>Within the <a href="https://console.aws.amazon.com/ecs">ECS Console</a>, click “Repositories” on the navigation pane, and then click the “Create repository” button. Add the following repositories:</p>
<ol>
<li><code class="highlighter-rouge">microservicemovies/users-db-review</code></li>
<li><code class="highlighter-rouge">microservicemovies/movies-db-review</code></li>
<li><code class="highlighter-rouge">microservicemovies/users-service-review</code></li>
<li><code class="highlighter-rouge">microservicemovies/movies-service-review</code></li>
<li><code class="highlighter-rouge">microservicemovies/web-service-review</code></li>
<li><code class="highlighter-rouge">microservicemovies/swagger-review</code></li>
</ol>
<p><a href="/assets/img/blog/on-demand-environments/aws-ecr-repos.png"><img src="/assets/img/blog/on-demand-environments/aws-ecr-repos.png" alt="aws ecr repos" /></a></p>
<h3 id="application-load-balancer">Application Load Balancer</h3>
<p><a href="https://aws.amazon.com/elasticloadbalancing/">Elastic Load Balancing</a>, as of writing, supports two types of load balancers - <em>Classic</em> and <em>Application</em>. Either will work with ECS, but the Application Load Balancer (ALB) is the better of the two since it:</p>
<ol>
<li>Dynamically maps container services to ports</li>
<li>Distributes traffic evenly across the entire ECS Service</li>
<li>Runs status health checks against each service</li>
<li>Allows for zero-downtime deploys</li>
</ol>
<p>To set up, navigate to the <a href="https://console.aws.amazon.com/ec2/">EC2 Dashboard</a>, update the region (if necessary), and then click “Load Balancers” in the navigation pane. Click the “Create Load Balancer” button. Select “Application Load Balancer”, and then go through each of the steps to configure the load balancer…</p>
<ol>
<li><em>Configure Load Balancer</em>:
<ul>
<li>“Name”: <code class="highlighter-rouge">microservicemovies-review</code></li>
<li>“VPC”: Select the VPC that was just created</li>
<li>“Availability Zones”: Select at least two available subnets</li>
</ul>
</li>
<li><em>Configure Security Settings</em>: Skip this for now</li>
<li><em>Configure Security Groups</em>: Select the Security Group that was just created</li>
<li><em>Configure Routing</em>:
<ul>
<li>“Name”: <code class="highlighter-rouge">review-default</code></li>
<li>“Port”: <code class="highlighter-rouge">80</code></li>
<li>“Path”: <code class="highlighter-rouge">/</code></li>
</ul>
</li>
<li><em>Register Targets</em>: Do not assign any instances manually since this will be managed by ECS</li>
</ol>
<p><a href="/assets/img/blog/on-demand-environments/aws-load-balancer.png"><img src="/assets/img/blog/on-demand-environments/aws-load-balancer.png" alt="aws load balancer" /></a></p>
<p>Along with the Application Load Balancer, this will also create a default Target Group called <code class="highlighter-rouge">review-default</code>:</p>
<p><a href="/assets/img/blog/on-demand-environments/aws-default-target-group.png"><img src="/assets/img/blog/on-demand-environments/aws-default-target-group.png" alt="aws default target group" /></a></p>
<h3 id="security-group">Security Group</h3>
<p>Finally, let’s add some ports to work with to the Security Group. Within the <a href="https://console.aws.amazon.com/ec2/">EC2 Dashboard</a>, click “Security Groups” in the navigation pane, and then select the Security Group that was just created. On the “Inbound Rules” pane, click the “Edit” button and the “Add another rule button”:</p>
<ol>
<li>“Type”: “Custom TCP Rule”</li>
<li>“Protocol”: “TCP (6)”</li>
<li>“Port Range”: <code class="highlighter-rouge">30000-50000</code></li>
<li>“Source”: <code class="highlighter-rouge">0.0.0.0/0</code></li>
</ol>
<p>Click the “Save” button.</p>
<p><a href="/assets/img/blog/on-demand-environments/aws-security-groups.png"><img src="/assets/img/blog/on-demand-environments/aws-security-groups.png" alt="aws security groups" /></a></p>
<p>That’s it for the basic AWS resources.</p>
<h2 id="circle-ci-setup">Circle CI Setup</h2>
<p>Next, <a href="https://circleci.com/signup/">sign up</a> for Circle CI (if necessary) and follow the basic <a href="https://circleci.com/docs/2.0/first-steps/">steps</a> to configure Circle 2.0, making sure to enable access to your GitHub repos. Then, create a new folder called “.circleci” in the project root of “microservice-movies” and add a <em>config.yml</em> file to that directory:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s">2</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">docker</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">image</span><span class="pi">:</span> <span class="s">docker:17.03.2-ce-git</span>
<span class="na">working_directory</span><span class="pi">:</span> <span class="s">~/microservice-movies</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">NODE_ENV</span><span class="pi">:</span> <span class="s">test</span>
<span class="pi">-</span> <span class="na">REACT_APP_USERS_SERVICE_URL</span><span class="pi">:</span> <span class="s">http://localhost:3000</span>
<span class="pi">-</span> <span class="na">REACT_APP_MOVIES_SERVICE_URL</span><span class="pi">:</span> <span class="s">http://localhost:3001</span>
<span class="na">parallelism</span><span class="pi">:</span> <span class="s">1</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">checkout</span>
<span class="pi">-</span> <span class="na">setup_remote_docker</span><span class="pi">:</span>
<span class="na">reusable</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">exclusive</span><span class="pi">:</span> <span class="no">false</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Install dependencies</span>
<span class="na">command</span><span class="pi">:</span> <span class="pi">|</span>
<span class="no">apk add --no-cache \</span>
<span class="no">py-pip=9.0.0-r1 \</span>
<span class="no">bash \</span>
<span class="no">jq \</span>
<span class="no">curl \</span>
<span class="no">nodejs</span>
<span class="no">pip install \</span>
<span class="no">docker-compose==1.12.0 \</span>
<span class="no">awscli==1.11.76</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Build Docker images</span>
<span class="na">command</span><span class="pi">:</span> <span class="s">docker-compose -f docker-compose-review.yml build</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Spin up Docker containers</span>
<span class="na">command</span><span class="pi">:</span> <span class="s">docker-compose -f docker-compose-review.yml up -d</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Test the user service</span>
<span class="na">command</span><span class="pi">:</span> <span class="s">docker-compose -f docker-compose-review.yml run users-service-review npm test</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Test the movies service</span>
<span class="na">command</span><span class="pi">:</span> <span class="s">docker-compose -f docker-compose-review.yml run movies-service-review npm test</span>
</code></pre></div></div>
<p>Review this file. Refer to the Circle <a href="https://circleci.com/docs/2.0/">documentation</a> as needed. Commit and push your code to GitHub once done. On Circle, navigate to the “Add Projects” page and click the “Build Project” button next to your project. This will trigger a new build, which should pass.</p>
<h2 id="aws-sdk-setup">AWS SDK Setup</h2>
<p>Since the microservice stack is built with Node, we’ll develop the deployment script in JavaScript with the <a href="https://www.npmjs.com/package/aws-sdk">AWS JavaScript SDK</a>.</p>
<p>Add a <em>package.json</em> to the project root:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"microservice-movies"</span><span class="p">,</span><span class="w">
</span><span class="s2">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"aws-sdk"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.114.0"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Install the dependency:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install
</code></pre></div></div>
<p>Add a new folder to the project root called “ecs”, and then create a new folder called “scripts” within that folder. Finally, add a new file called <em>setup.js</em> to “scripts”:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">AWS</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'aws-sdk'</span><span class="p">);</span>
<span class="c1">// globals</span>
<span class="kd">const</span> <span class="nx">AWS_ACCOUNT_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCOUNT_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_ACCESS_KEY_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_USERNAME</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_USERNAME</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_CONFIG_REGION</span> <span class="o">=</span> <span class="s1">'us-west-2'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">clusterName</span> <span class="o">=</span> <span class="s1">'microservicemovies-review'</span><span class="p">;</span>
<span class="c1">// config</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">Config</span><span class="p">();</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">accessKeyId</span> <span class="o">=</span> <span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">secretAccessKey</span> <span class="o">=</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">region</span> <span class="o">=</span> <span class="nx">AWS_CONFIG_REGION</span><span class="p">;</span>
<span class="c1">// init aws services</span>
<span class="kd">const</span> <span class="nx">ecs</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">ECS</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">iam</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">IAM</span><span class="p">();</span>
<span class="c1">// methods</span>
<span class="kd">function</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span> <span class="na">UserName</span><span class="p">:</span> <span class="nx">AWS_USERNAME</span> <span class="p">};</span>
<span class="nx">iam</span><span class="p">.</span><span class="nx">getUser</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">confirmRegion</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">region</span> <span class="o">!==</span> <span class="nx">AWS_CONFIG_REGION</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">reject</span><span class="p">(</span><span class="s1">'Something went wrong!'</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">AWS_CONFIG_REGION</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">getCluster</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span> <span class="na">clusters</span><span class="p">:</span> <span class="p">[</span> <span class="nx">clusterName</span> <span class="p">]</span> <span class="p">};</span>
<span class="nx">ecs</span><span class="p">.</span><span class="nx">describeClusters</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="c1">// main</span>
<span class="k">return</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nx">UserName</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">confirmRegion</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">region</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`AWS Region -> </span><span class="p">${</span><span class="nx">region</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">getCluster</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">cluster</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">cluster</span><span class="p">.</span><span class="nx">clusters</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Cluster does not exist!'</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`ECS Cluster -> </span><span class="p">${</span><span class="nx">cluster</span><span class="p">.</span><span class="nx">clusters</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">clusterName</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
</code></pre></div></div>
<p>In essence, this confirms that the AWS user credentials are correct and that the AWS Region and ECS Cluster are configured properly.</p>
<p>Set the following environment variables locally:</p>
<ol>
<li><code class="highlighter-rouge">AWS_ACCOUNT_ID</code></li>
<li><code class="highlighter-rouge">AWS_ACCESS_KEY_ID</code></li>
<li><code class="highlighter-rouge">AWS_SECRET_ACCESS_KEY</code></li>
<li><code class="highlighter-rouge">AWS_USERNAME</code></li>
</ol>
<p>Run the script:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>node ecs/scripts/setup.js
</code></pre></div></div>
<p>You should see:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Welcome AWS_USERNAME!
AWS Region -> us-west-2
ECS Cluster -> microservicemovies-review
</code></pre></div></div>
<p>Add another step to the bottom of <em>.circleci/config.yml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">run</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Deploy</span>
<span class="na">command</span><span class="pi">:</span> <span class="pi">|</span>
<span class="no">npm install</span>
<span class="no">node ecs/scripts/setup.js</span>
</code></pre></div></div>
<p>Add the following <a href="https://circleci.com/docs/2.0/env-vars/#adding-environment-variables-in-the-app">environment variables</a> to Circle so that we can properly configure the AWS SDK:</p>
<ol>
<li><code class="highlighter-rouge">AWS_ACCOUNT_ID</code></li>
<li><code class="highlighter-rouge">AWS_ACCESS_KEY_ID</code></li>
<li><code class="highlighter-rouge">AWS_SECRET_ACCESS_KEY</code></li>
<li><code class="highlighter-rouge">AWS_USERNAME</code></li>
</ol>
<p>Commit your changes, and then push to GitHub to trigger a new build. Make sure it passes before moving on.</p>
<h2 id="deployment-script">Deployment Script</h2>
<p>Moving right along, let’s start building the deployment scripts to set up the AWS resources and deploy the app.</p>
<p>Main Steps:</p>
<ol>
<li>Tag and push images to ECR</li>
<li>Get open port for the Listener</li>
<li>Register Task Definitions</li>
<li>Create Target Groups</li>
<li>Add the Listener and Rules</li>
<li>Create new Services</li>
</ol>
<h3 id="1-tag-and-push-images-to-ecr">(1) Tag and push images to ECR</h3>
<p>Create a new file in “ecs/scripts” called <em>ecr.sh</em>:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>
<span class="c"># config</span>
<span class="nb">set</span> <span class="nt">-e</span>
<span class="nv">ECS_REGION</span><span class="o">=</span><span class="s2">"us-west-2"</span>
<span class="nv">NAMESPACE</span><span class="o">=</span><span class="s2">"microservicemovies"</span>
<span class="nv">IMAGE_BASE</span><span class="o">=</span><span class="s2">"microservicemovies"</span>
<span class="nv">ECR_URI</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">AWS_ACCOUNT_ID</span><span class="k">}</span><span class="s2">.dkr.ecr.</span><span class="k">${</span><span class="nv">ECS_REGION</span><span class="k">}</span><span class="s2">.amazonaws.com"</span>
<span class="nv">SHORT_GIT_HASH</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="nv">$CIRCLE_SHA1</span> | cut <span class="nt">-c</span> <span class="nt">-7</span><span class="k">)</span>
<span class="nv">TAG</span><span class="o">=</span><span class="nv">$SHORT_GIT_HASH</span>
<span class="c"># helpers</span>
configure_aws_cli<span class="o">()</span> <span class="o">{</span>
<span class="nb">echo</span> <span class="s2">"Configuring AWS..."</span>
aws <span class="nt">--version</span>
aws configure <span class="nb">set </span>default.region <span class="nv">$ECS_REGION</span>
aws configure <span class="nb">set </span>default.output json
<span class="nb">echo</span> <span class="s2">"AWS configured!"</span>
<span class="o">}</span>
tag_and_push_images<span class="o">()</span> <span class="o">{</span>
<span class="nb">echo</span> <span class="s2">"Tagging and pushing images..."</span>
<span class="k">$(</span>aws ecr get-login <span class="nt">--region</span> <span class="s2">"</span><span class="k">${</span><span class="nv">ECS_REGION</span><span class="k">}</span><span class="s2">"</span><span class="k">)</span>
<span class="c"># tag</span>
docker tag <span class="k">${</span><span class="nv">IMAGE_BASE</span><span class="k">}</span>_users-db-review <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/users-db-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
docker tag <span class="k">${</span><span class="nv">IMAGE_BASE</span><span class="k">}</span>_movies-db-review <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/movies-db-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
docker tag <span class="k">${</span><span class="nv">IMAGE_BASE</span><span class="k">}</span>_users-service-review <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/users-service-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
docker tag <span class="k">${</span><span class="nv">IMAGE_BASE</span><span class="k">}</span>_movies-service-review <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/movies-service-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
docker tag <span class="k">${</span><span class="nv">IMAGE_BASE</span><span class="k">}</span>_web-service-review <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/web-service-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
docker tag <span class="k">${</span><span class="nv">IMAGE_BASE</span><span class="k">}</span>_swagger-review <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/swagger-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
<span class="c"># push</span>
docker push <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/users-db-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
docker push <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/movies-db-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
docker push <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/users-service-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
docker push <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/movies-service-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
docker push <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/web-service-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
docker push <span class="k">${</span><span class="nv">ECR_URI</span><span class="k">}</span>/<span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span>/swagger-review:<span class="k">${</span><span class="nv">TAG</span><span class="k">}</span>
<span class="nb">echo</span> <span class="s2">"Images tagged and pushed!"</span>
<span class="o">}</span>
<span class="c"># main</span>
configure_aws_cli
tag_and_push_images
</code></pre></div></div>
<p>Each deploy is associated with a different commit and, thus, a different version of the code. To link the commit back to a specific image, we just used part of the Git commit SHA as the image tag:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">SHORT_GIT_HASH</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="nv">$CIRCLE_SHA1</span> | cut <span class="nt">-c</span> <span class="nt">-7</span><span class="k">)</span>
<span class="nv">TAG</span><span class="o">=</span><span class="nv">$SHORT_GIT_HASH</span>
</code></pre></div></div>
<blockquote>
<p><strong>NOTE:</strong> Instead of using the associated Git commit SHA, you could get a bit creative and build a Heroku-like random name generator.</p>
</blockquote>
<p>Update the <code class="highlighter-rouge">Deploy</code> command in <em>.circle/config.yml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">run</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Deploy</span>
<span class="na">command</span><span class="pi">:</span> <span class="pi">|</span>
<span class="no">npm install</span>
<span class="no">node ecs/scripts/setup.js</span>
<span class="no">sh ecs/scripts/ecr.sh</span>
</code></pre></div></div>
<p>Commit and push your code to GitHub to trigger a new Circle build. Once done, ensure the images are up on ECS with the appropriate tag:</p>
<p><a href="/assets/img/blog/on-demand-environments/aws-ecr-users-db-review-image.png"><img src="/assets/img/blog/on-demand-environments/aws-ecr-users-db-review-image.png" alt="aws ecr users db review image" /></a></p>
<h3 id="2-get-open-port-for-the-listener">(2) Get open port for the Listener</h3>
<p>Remember how we set a range of open ports, <code class="highlighter-rouge">30000</code> - <code class="highlighter-rouge">50000</code>, when we set up the Security Group? Well, each new environment will be assigned to a port within this range.</p>
<p>Add a new file called <em>listener.js</em> to “ecs/scripts”:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">AWS</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'aws-sdk'</span><span class="p">);</span>
<span class="c1">// globals</span>
<span class="kd">const</span> <span class="nx">AWS_ACCOUNT_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCOUNT_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_ACCESS_KEY_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_USERNAME</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_USERNAME</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_CONFIG_REGION</span> <span class="o">=</span> <span class="s1">'us-west-2'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">LOAD_BALANCER_ARN</span> <span class="o">=</span> <span class="s1">'UPDATE_ME'</span><span class="p">;</span>
<span class="c1">// config</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">Config</span><span class="p">();</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">accessKeyId</span> <span class="o">=</span> <span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">secretAccessKey</span> <span class="o">=</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">region</span> <span class="o">=</span> <span class="nx">AWS_CONFIG_REGION</span><span class="p">;</span>
<span class="c1">// init aws services</span>
<span class="kd">const</span> <span class="nx">elbv2</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">ELBv2</span><span class="p">();</span>
<span class="c1">// methods</span>
<span class="kd">function</span> <span class="nx">getPort</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">LoadBalancerArn</span><span class="p">:</span> <span class="nx">LOAD_BALANCER_ARN</span>
<span class="p">};</span>
<span class="nx">elbv2</span><span class="p">.</span><span class="nx">describeListeners</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="kd">const</span> <span class="nx">max</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">Listeners</span><span class="p">.</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">prev</span><span class="p">,</span> <span class="nx">current</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span><span class="nx">prev</span><span class="p">.</span><span class="nx">Port</span> <span class="o">></span> <span class="nx">current</span><span class="p">.</span><span class="nx">Port</span><span class="p">)</span> <span class="p">?</span> <span class="nx">prev</span> <span class="p">:</span> <span class="nx">current</span><span class="p">;</span>
<span class="p">});</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">parseInt</span><span class="p">(</span><span class="nx">max</span><span class="p">.</span><span class="nx">Port</span><span class="p">)</span> <span class="o">===</span> <span class="mi">80</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">resolve</span><span class="p">(</span><span class="mi">30000</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">port</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">max</span><span class="p">.</span><span class="nx">Port</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">port</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">getPort</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Add then update the following variable:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">LOAD_BALANCER_ARN</span> <span class="o">=</span> <span class="s1">'UPDATE_ME'</span><span class="p">;</span>
</code></pre></div></div>
<p>Then, take note of the following <code class="highlighter-rouge">if</code> statement:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nb">parseInt</span><span class="p">(</span><span class="nx">max</span><span class="p">.</span><span class="nx">Port</span><span class="p">)</span> <span class="o">===</span> <span class="mi">80</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">resolve</span><span class="p">(</span><span class="mi">30000</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">port</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">max</span><span class="p">.</span><span class="nx">Port</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">port</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Why do we need this?</p>
<p>Since the default Target Group, <code class="highlighter-rouge">review-default</code>, was configured to listen on port 80, we need to account for it if it’s the only Listener set up since we’re bumping the port by 1 each time.</p>
<p>We’ll test this out in the next section…</p>
<h3 id="3-register-task-definitions">(3) Register Task Definitions</h3>
<p>A <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html">Task Definition</a> is used to create an application from one or more containers. It’s similar to a Docker Compose file.</p>
<p>Steps:</p>
<ol>
<li>Create Task Definition files</li>
<li>Create script to register Task Definitions</li>
</ol>
<h4 id="create-task-definition-files">Create Task Definition files</h4>
<p>Create a new directory called “tasks” within “ecs”, and then add the following files:</p>
<ol>
<li><em>users-review_task.js</em></li>
<li><em>movies-review_task.js</em></li>
<li><em>web-review_task.js</em></li>
</ol>
<h5 id="users---users-review_taskjs">Users - <em>users-review_task.js</em></h5>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createUsersTaskDefinition</span><span class="p">(</span><span class="nx">accountID</span><span class="p">,</span> <span class="nx">region</span><span class="p">,</span> <span class="nx">tag</span><span class="p">,</span> <span class="nx">family</span><span class="p">,</span> <span class="nx">revision</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">taskDefinition</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">containerDefinitions</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'users-service-review'</span><span class="p">,</span>
<span class="na">image</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">accountID</span><span class="p">}</span><span class="s2">.dkr.ecr.</span><span class="p">${</span><span class="nx">region</span><span class="p">}</span><span class="s2">.amazonaws.com\/microservicemovies\/users-service-review:</span><span class="p">${</span><span class="nx">tag</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="na">essential</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">memoryReservation</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">cpu</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">portMappings</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">containerPort</span><span class="p">:</span> <span class="mi">3000</span><span class="p">,</span>
<span class="na">hostPort</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="na">protocol</span><span class="p">:</span> <span class="s1">'tcp'</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">environment</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'DATABASE_URL'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'postgres://postgres:postgres@users-db-review:5432/users_dev'</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'DATABASE_TEST_URL'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'postgres://postgres:postgres@users-db-review:5432/users_test'</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'NODE_ENV'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'test'</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'TOKEN_SECRET'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'changeme'</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">links</span><span class="p">:</span> <span class="p">[</span>
<span class="s1">'users-db-review'</span>
<span class="p">],</span>
<span class="na">logConfiguration</span><span class="p">:</span> <span class="p">{</span>
<span class="na">logDriver</span><span class="p">:</span> <span class="s1">'awslogs'</span><span class="p">,</span>
<span class="na">options</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'awslogs-group'</span><span class="p">:</span> <span class="s1">'microservicemovies'</span><span class="p">,</span>
<span class="s1">'awslogs-region'</span><span class="p">:</span> <span class="nx">region</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'users-db-review'</span><span class="p">,</span>
<span class="na">image</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">accountID</span><span class="p">}</span><span class="s2">.dkr.ecr.</span><span class="p">${</span><span class="nx">region</span><span class="p">}</span><span class="s2">.amazonaws.com\/microservicemovies\/users-db-review:</span><span class="p">${</span><span class="nx">tag</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="na">essential</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">memoryReservation</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">cpu</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">portMappings</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">containerPort</span><span class="p">:</span> <span class="mi">5432</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">environment</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'POSTGRES_USER'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'postgres'</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'POSTGRES_PASSWORD'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'postgres'</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">logConfiguration</span><span class="p">:</span> <span class="p">{</span>
<span class="na">logDriver</span><span class="p">:</span> <span class="s1">'awslogs'</span><span class="p">,</span>
<span class="na">options</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'awslogs-group'</span><span class="p">:</span> <span class="s1">'microservicemovies'</span><span class="p">,</span>
<span class="s1">'awslogs-region'</span><span class="p">:</span> <span class="nx">region</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">family</span><span class="p">:</span> <span class="s1">'microservicemovies-review-users-td'</span>
<span class="p">};</span>
<span class="k">return</span> <span class="nx">taskDefinition</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">createUsersTaskDefinition</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Most of this should be fairly straightforward since the container definition relates back to the Docker Compose file. Review the <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html">Task Definition Parameters</a> guide for more info.</p>
<p>Two things to note:</p>
<ol>
<li>We set the host port for the users service to <code class="highlighter-rouge">0</code> so that a port is dynamically assigned when the Task is fired up.</li>
<li>We also configured logs, via <a href="http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_LogConfiguration.html">LogConfiguration</a>, to pipe logs to <a href="https://console.aws.amazon.com/cloudwatch">CloudWatch</a>. To set up, we need to create a new <a href="http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CloudWatchLogsConcepts.html">Log Group</a>. Simply navigate to <a href="https://console.aws.amazon.com/cloudwatch">CloudWatch</a>, click “Logs” on the navigation pane, click the “Actions” drop-down button, and then select “Create log group”. Name the group <code class="highlighter-rouge">microservicemovies-review</code>.</li>
</ol>
<h5 id="movies---movies-review_taskjs">Movies - <em>movies-review_task.js</em></h5>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createMoviesTaskDefinition</span><span class="p">(</span><span class="nx">accountID</span><span class="p">,</span> <span class="nx">region</span><span class="p">,</span> <span class="nx">tag</span><span class="p">,</span> <span class="nx">family</span><span class="p">,</span> <span class="nx">revision</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">taskDefinition</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">containerDefinitions</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'movies-service-review'</span><span class="p">,</span>
<span class="na">image</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">accountID</span><span class="p">}</span><span class="s2">.dkr.ecr.</span><span class="p">${</span><span class="nx">region</span><span class="p">}</span><span class="s2">.amazonaws.com\/microservicemovies\/movies-service-review:</span><span class="p">${</span><span class="nx">tag</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="na">essential</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">memoryReservation</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">cpu</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">portMappings</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">containerPort</span><span class="p">:</span> <span class="mi">3000</span><span class="p">,</span>
<span class="na">hostPort</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="na">protocol</span><span class="p">:</span> <span class="s1">'tcp'</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">environment</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'DATABASE_URL'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'postgres://postgres:postgres@movies-db-review:5432/movies_dev'</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'DATABASE_TEST_URL'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'postgres://postgres:postgres@movies-db-review:5432/movies_test'</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'NODE_ENV'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'test'</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'TOKEN_SECRET'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'changeme'</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">links</span><span class="p">:</span> <span class="p">[</span>
<span class="s1">'movies-db-review'</span>
<span class="p">],</span>
<span class="na">logConfiguration</span><span class="p">:</span> <span class="p">{</span>
<span class="na">logDriver</span><span class="p">:</span> <span class="s1">'awslogs'</span><span class="p">,</span>
<span class="na">options</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'awslogs-group'</span><span class="p">:</span> <span class="s1">'microservicemovies'</span><span class="p">,</span>
<span class="s1">'awslogs-region'</span><span class="p">:</span> <span class="nx">region</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'movies-db-review'</span><span class="p">,</span>
<span class="na">image</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">accountID</span><span class="p">}</span><span class="s2">.dkr.ecr.</span><span class="p">${</span><span class="nx">region</span><span class="p">}</span><span class="s2">.amazonaws.com\/microservicemovies\/movies-db-review:</span><span class="p">${</span><span class="nx">tag</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="na">essential</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">memoryReservation</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">cpu</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">portMappings</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">containerPort</span><span class="p">:</span> <span class="mi">5432</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">environment</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'POSTGRES_USER'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'postgres'</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'POSTGRES_PASSWORD'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'postgres'</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">logConfiguration</span><span class="p">:</span> <span class="p">{</span>
<span class="na">logDriver</span><span class="p">:</span> <span class="s1">'awslogs'</span><span class="p">,</span>
<span class="na">options</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'awslogs-group'</span><span class="p">:</span> <span class="s1">'microservicemovies'</span><span class="p">,</span>
<span class="s1">'awslogs-region'</span><span class="p">:</span> <span class="nx">region</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'swagger-review'</span><span class="p">,</span>
<span class="na">image</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">accountID</span><span class="p">}</span><span class="s2">.dkr.ecr.</span><span class="p">${</span><span class="nx">region</span><span class="p">}</span><span class="s2">.amazonaws.com\/microservicemovies\/swagger-review:</span><span class="p">${</span><span class="nx">tag</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="na">essential</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">memoryReservation</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">cpu</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">portMappings</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">containerPort</span><span class="p">:</span> <span class="mi">3001</span><span class="p">,</span>
<span class="na">hostPort</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="na">protocol</span><span class="p">:</span> <span class="s1">'tcp'</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">environment</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'NODE_ENV'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'test'</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">logConfiguration</span><span class="p">:</span> <span class="p">{</span>
<span class="na">logDriver</span><span class="p">:</span> <span class="s1">'awslogs'</span><span class="p">,</span>
<span class="na">options</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'awslogs-group'</span><span class="p">:</span> <span class="s1">'microservicemovies'</span><span class="p">,</span>
<span class="s1">'awslogs-region'</span><span class="p">:</span> <span class="nx">region</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">family</span><span class="p">:</span> <span class="s1">'microservicemovies-review-movies-td'</span>
<span class="p">};</span>
<span class="k">return</span> <span class="nx">taskDefinition</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">createMoviesTaskDefinition</span>
<span class="p">};</span>
</code></pre></div></div>
<p>The first two containers should be almost identical to the containers in the previous Task Definition. Review the Swagger container definition.</p>
<h5 id="web---web-review_taskjs">Web - <em>web-review_task.js</em></h5>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createWebTaskDefinition</span><span class="p">(</span><span class="nx">accountID</span><span class="p">,</span> <span class="nx">region</span><span class="p">,</span> <span class="nx">tag</span><span class="p">,</span> <span class="nx">usersURL</span><span class="p">,</span> <span class="nx">moviesURL</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">taskDefinition</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">containerDefinitions</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'web-service-review'</span><span class="p">,</span>
<span class="na">image</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">accountID</span><span class="p">}</span><span class="s2">.dkr.ecr.</span><span class="p">${</span><span class="nx">region</span><span class="p">}</span><span class="s2">.amazonaws.com\/microservicemovies\/web-service-review:</span><span class="p">${</span><span class="nx">tag</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="na">essential</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">memoryReservation</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">cpu</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span>
<span class="na">portMappings</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">containerPort</span><span class="p">:</span> <span class="mi">9000</span><span class="p">,</span>
<span class="na">hostPort</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="na">protocol</span><span class="p">:</span> <span class="s1">'tcp'</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">environment</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'NODE_ENV'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="s1">'test'</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'REACT_APP_USERS_SERVICE_URL'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="nx">usersURL</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'REACT_APP_MOVIES_SERVICE_URL'</span><span class="p">,</span>
<span class="na">value</span><span class="p">:</span> <span class="nx">moviesURL</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">logConfiguration</span><span class="p">:</span> <span class="p">{</span>
<span class="na">logDriver</span><span class="p">:</span> <span class="s1">'awslogs'</span><span class="p">,</span>
<span class="na">options</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'awslogs-group'</span><span class="p">:</span> <span class="s1">'microservicemovies'</span><span class="p">,</span>
<span class="s1">'awslogs-region'</span><span class="p">:</span> <span class="nx">region</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="p">],</span>
<span class="na">family</span><span class="p">:</span> <span class="s1">'microservicemovies-review-web-td'</span>
<span class="p">};</span>
<span class="k">return</span> <span class="nx">taskDefinition</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">createWebTaskDefinition</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Notice how we’re passing in values for the environment variables. You could also use HashiCorp’s <a href="https://www.vaultproject.io/">Vault</a> project to better manage secrets and variables.</p>
<h4 id="create-script-to-register-task-definitions">Create script to register Task Definitions</h4>
<p>Next, add a new file called <em>tasks.js</em> to “ecs/scripts”:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">AWS</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'aws-sdk'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">createUsersTaskDefinition</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../tasks/users-review_task'</span><span class="p">).</span><span class="nx">createUsersTaskDefinition</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">createMoviesTaskDefinition</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../tasks/movies-review_task'</span><span class="p">).</span><span class="nx">createMoviesTaskDefinition</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">createWebTaskDefinition</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../tasks/web-review_task'</span><span class="p">).</span><span class="nx">createWebTaskDefinition</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">port</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./listener'</span><span class="p">).</span><span class="nx">getPort</span><span class="p">;</span>
<span class="c1">// globals</span>
<span class="kd">const</span> <span class="nx">AWS_ACCOUNT_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCOUNT_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_ACCESS_KEY_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_USERNAME</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_USERNAME</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_CONFIG_REGION</span> <span class="o">=</span> <span class="s1">'us-west-2'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">SHORT_GIT_HASH</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CIRCLE_SHA1</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">7</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">LOAD_BALANCER_DNS</span> <span class="o">=</span> <span class="s1">'http://microservicemovies-review-476947634.us-west-2.elb.amazonaws.com'</span><span class="p">;</span>
<span class="c1">// config</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">Config</span><span class="p">();</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">accessKeyId</span> <span class="o">=</span> <span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">secretAccessKey</span> <span class="o">=</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">region</span> <span class="o">=</span> <span class="nx">AWS_CONFIG_REGION</span><span class="p">;</span>
<span class="c1">// init aws services</span>
<span class="kd">const</span> <span class="nx">ecs</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">ECS</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">iam</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">IAM</span><span class="p">();</span>
<span class="c1">// methods</span>
<span class="kd">function</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span> <span class="na">UserName</span><span class="p">:</span> <span class="nx">AWS_USERNAME</span> <span class="p">};</span>
<span class="nx">iam</span><span class="p">.</span><span class="nx">getUser</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">registerTaskDef</span><span class="p">(</span><span class="nx">task</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="nx">task</span><span class="p">;</span>
<span class="nx">ecs</span><span class="p">.</span><span class="nx">registerTaskDefinition</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">registerUsersTD</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">task</span> <span class="o">=</span> <span class="nx">createUsersTaskDefinition</span><span class="p">(</span><span class="nx">AWS_ACCOUNT_ID</span><span class="p">,</span> <span class="nx">AWS_CONFIG_REGION</span><span class="p">,</span> <span class="nx">SHORT_GIT_HASH</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">registerTaskDef</span><span class="p">(</span><span class="nx">task</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Task Registered!'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">taskDefinition</span><span class="p">.</span><span class="nx">taskDefinitionArn</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">registerMoviesTD</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">task</span> <span class="o">=</span> <span class="nx">createMoviesTaskDefinition</span><span class="p">(</span><span class="nx">AWS_ACCOUNT_ID</span><span class="p">,</span> <span class="nx">AWS_CONFIG_REGION</span><span class="p">,</span> <span class="nx">SHORT_GIT_HASH</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">registerTaskDef</span><span class="p">(</span><span class="nx">task</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Task Registered!'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">taskDefinition</span><span class="p">.</span><span class="nx">taskDefinitionArn</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">registerWebTD</span><span class="p">(</span><span class="nx">usersURL</span><span class="p">,</span> <span class="nx">moviesURL</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">task</span> <span class="o">=</span> <span class="nx">createWebTaskDefinition</span><span class="p">(</span><span class="nx">AWS_ACCOUNT_ID</span><span class="p">,</span> <span class="nx">AWS_CONFIG_REGION</span><span class="p">,</span> <span class="nx">SHORT_GIT_HASH</span><span class="p">,</span> <span class="nx">usersURL</span><span class="p">,</span> <span class="nx">moviesURL</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">registerTaskDef</span><span class="p">(</span><span class="nx">task</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Task Registered!'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">taskDefinition</span><span class="p">.</span><span class="nx">taskDefinitionArn</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="c1">// main</span>
<span class="k">return</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nx">UserName</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">port</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">port</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">usersURL</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">LOAD_BALANCER_DNS</span><span class="p">}</span><span class="s2">:</span><span class="p">${</span><span class="nx">port</span><span class="p">}</span><span class="s2">/users`</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">moviesURL</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">LOAD_BALANCER_DNS</span><span class="p">}</span><span class="s2">:</span><span class="p">${</span><span class="nx">port</span><span class="p">}</span><span class="s2">/movies`</span><span class="p">;</span>
<span class="nx">registerUsersTD</span><span class="p">();</span>
<span class="nx">registerMoviesTD</span><span class="p">();</span>
<span class="nx">registerWebTD</span><span class="p">(</span><span class="nx">usersURL</span><span class="p">,</span> <span class="nx">moviesURL</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
</code></pre></div></div>
<p>Here, we pulled in the <code class="highlighter-rouge">getPort</code> function from the <em>listener.js</em> file, created the individual Task Definitions, and then passed the definitions to the <code class="highlighter-rouge">registerTaskDefinition</code> function to register them on AWS. Review the official AWS SDK <a href="http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ECS.html#registerTaskDefinition-property">documentation</a> for more info.</p>
<p>Update the <code class="highlighter-rouge">Deploy</code> command in <em>.circle/config.yml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">run</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Deploy</span>
<span class="na">command</span><span class="pi">:</span> <span class="pi">|</span>
<span class="no">npm install</span>
<span class="no">node ecs/scripts/setup.js</span>
<span class="no">sh ecs/scripts/ecr.sh</span>
<span class="no">node ecs/scripts/tasks.js</span>
</code></pre></div></div>
<p>Commit and push your code to GitHub. Once the Circle build passes, make sure the Task Definitions were created:</p>
<p><a href="/assets/img/blog/on-demand-environments/aws-ecs-task-definitions.png"><img src="/assets/img/blog/on-demand-environments/aws-ecs-task-definitions.png" alt="aws ecs task definitions" /></a></p>
<p>Also, make sure the <code class="highlighter-rouge">REACT_APP_USERS_SERVICE_URL</code> and <code class="highlighter-rouge">REACT_APP_MOVIES_SERVICE_URL</code> environment variables were added correctly to the <code class="highlighter-rouge">microservicemovies-review-web-td</code> Task Definition by clicking the Task Definition name, selecting the latest revision, and then expanding the <code class="highlighter-rouge">web-service-review</code> container:</p>
<p><a href="/assets/img/blog/on-demand-environments/aws-ecs-web-task-definition.png"><img src="/assets/img/blog/on-demand-environments/aws-ecs-web-task-definition.png" alt="aws ecs web task definition" /></a></p>
<h3 id="4-create-target-groups">(4) Create Target Groups</h3>
<p><a href="http://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html">Target Group</a>s are used to link the Application Load Balancer (ALB) to the container instances, which we just defined.</p>
<p>Create a new script in “scripts” called <em>alb.js</em>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">AWS</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'aws-sdk'</span><span class="p">);</span>
<span class="c1">// globals</span>
<span class="kd">const</span> <span class="nx">AWS_ACCOUNT_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCOUNT_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_ACCESS_KEY_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_USERNAME</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_USERNAME</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_CONFIG_REGION</span> <span class="o">=</span> <span class="s1">'us-west-2'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">SHORT_GIT_HASH</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CIRCLE_SHA1</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">7</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">VPC_ID</span><span class="o">=</span><span class="s1">'UPDATE_ME'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">USERS_TARGET_GROUP_ARN</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">MOVIES_TARGET_GROUP_ARN</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">WEB_TARGET_GROUP_ARN</span><span class="p">;</span>
<span class="c1">// config</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">Config</span><span class="p">();</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">accessKeyId</span> <span class="o">=</span> <span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">secretAccessKey</span> <span class="o">=</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">region</span> <span class="o">=</span> <span class="nx">AWS_CONFIG_REGION</span><span class="p">;</span>
<span class="c1">// init aws services</span>
<span class="kd">const</span> <span class="nx">elbv2</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">ELBv2</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">iam</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">IAM</span><span class="p">();</span>
<span class="c1">// methods</span>
<span class="kd">function</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span> <span class="na">UserName</span><span class="p">:</span> <span class="nx">AWS_USERNAME</span> <span class="p">};</span>
<span class="nx">iam</span><span class="p">.</span><span class="nx">getUser</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">addTargetGroup</span><span class="p">(</span><span class="nx">service</span><span class="p">,</span> <span class="nx">port</span><span class="p">,</span> <span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">Name</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">SHORT_GIT_HASH</span><span class="p">}</span><span class="s2">-</span><span class="p">${</span><span class="nx">service</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="na">Port</span><span class="p">:</span> <span class="nx">port</span><span class="p">,</span>
<span class="na">Protocol</span><span class="p">:</span> <span class="s1">'HTTP'</span><span class="p">,</span>
<span class="na">VpcId</span><span class="p">:</span> <span class="nx">VPC_ID</span><span class="p">,</span>
<span class="na">HealthCheckPath</span><span class="p">:</span> <span class="nx">path</span>
<span class="p">};</span>
<span class="nx">elbv2</span><span class="p">.</span><span class="nx">createTargetGroup</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="c1">// main</span>
<span class="k">return</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nx">UserName</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addTargetGroup</span><span class="p">(</span><span class="s1">'users'</span><span class="p">,</span> <span class="s1">'3000'</span><span class="p">,</span> <span class="s1">'/users/ping'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">USERS_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Target Group Added!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addTargetGroup</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">,</span> <span class="s1">'3000'</span><span class="p">,</span> <span class="s1">'/movies/ping'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">MOVIES_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Target Group Added!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addTargetGroup</span><span class="p">(</span><span class="s1">'web'</span><span class="p">,</span> <span class="s1">'9000'</span><span class="p">,</span> <span class="s1">'/'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">WEB_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Target Group Added!'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
</code></pre></div></div>
<p>Make sure to add the correct VPC ID in the script, and then take note of the <code class="highlighter-rouge">addTargetGroup</code> function:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">addTargetGroup</span><span class="p">(</span><span class="nx">service</span><span class="p">,</span> <span class="nx">port</span><span class="p">,</span> <span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">Name</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">SHORT_GIT_HASH</span><span class="p">}</span><span class="s2">-</span><span class="p">${</span><span class="nx">service</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="na">Port</span><span class="p">:</span> <span class="nx">port</span><span class="p">,</span>
<span class="na">Protocol</span><span class="p">:</span> <span class="s1">'HTTP'</span><span class="p">,</span>
<span class="na">VpcId</span><span class="p">:</span> <span class="nx">VPC</span><span class="p">,</span>
<span class="na">HealthCheckPath</span><span class="p">:</span> <span class="nx">path</span>
<span class="p">};</span>
<span class="nx">elbv2</span><span class="p">.</span><span class="nx">createTargetGroup</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Again, the <code class="highlighter-rouge">name</code> should be unique, so we used a portion of the Git commit SHA in it. The <code class="highlighter-rouge">port</code> refers back to the container port, and the <code class="highlighter-rouge">HealthCheckPath</code> is used by the load balancer to ensure the container is running.</p>
<table>
<thead>
<tr>
<th>Task Name</th>
<th>Port</th>
<th>Path</th>
</tr>
</thead>
<tbody>
<tr>
<td>users-service-review</td>
<td>3000</td>
<td>/users/ping</td>
</tr>
<tr>
<td>movies-service-review</td>
<td>3000</td>
<td>/movies/ping</td>
</tr>
<tr>
<td>web-review</td>
<td>9000</td>
<td>/</td>
</tr>
</tbody>
</table>
<p>After the Target Groups are created, we grabbed the returned <a href="http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html">Amazon Resource Name</a> (ARN) and assigned it to a variable, which we’ll end up using shortly when we set up the Listeners. For more on registering Target Groups, review the official <a href="http://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html">documentation</a>.</p>
<p>Update the <code class="highlighter-rouge">Deploy</code> command in <em>.circle/config.yml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">run</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Deploy</span>
<span class="na">command</span><span class="pi">:</span> <span class="pi">|</span>
<span class="no">npm install</span>
<span class="no">node ecs/scripts/setup.js</span>
<span class="no">sh ecs/scripts/ecr.sh</span>
<span class="no">node ecs/scripts/tasks.js</span>
<span class="no">node ecs/scripts/alb.js</span>
</code></pre></div></div>
<p>Commit and push your changes to GitHub. Make sure the Circle build passes. Within the <a href="https://console.aws.amazon.com/ec2/">EC2 Dashboard</a>, click “Target Groups” on the navigation pane, and you should see three new Target Groups:</p>
<p><a href="/assets/img/blog/on-demand-environments/aws-target-groups.png"><img src="/assets/img/blog/on-demand-environments/aws-target-groups.png" alt="aws target groups" /></a></p>
<h3 id="5-add-the-listener-and-rules">(5) Add the Listener and Rules</h3>
<p>A <a href="http://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html">Listener</a> forwards traffic from the load balancer to a specific Target Group.</p>
<h4 id="add-listener">Add Listener</h4>
<p>Add the following function to <em>alb.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">addListener</span><span class="p">(</span><span class="nx">port</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">DefaultActions</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">TargetGroupArn</span><span class="p">:</span> <span class="nx">DEFAULT_TARGET_GROUP_ARN</span><span class="p">,</span>
<span class="na">Type</span><span class="p">:</span> <span class="s1">'forward'</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">LoadBalancerArn</span><span class="p">:</span> <span class="nx">LOAD_BALANCER_ARN</span><span class="p">,</span>
<span class="na">Port</span><span class="p">:</span> <span class="nx">port</span><span class="p">,</span>
<span class="na">Protocol</span><span class="p">:</span> <span class="s1">'HTTP'</span>
<span class="p">};</span>
<span class="nx">elbv2</span><span class="p">.</span><span class="nx">createListener</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And add then update the following variable:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">DEFAULT_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="s1">'UPDATE_ME'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">LOAD_BALANCER_ARN</span> <span class="o">=</span> <span class="s1">'UPDATE_ME'</span><span class="p">;</span>
</code></pre></div></div>
<h4 id="get-open-port">Get open port</h4>
<p>Before we can create a new Listener, we need to find an open port on the Application Load Balancer to add it to. So, import the <code class="highlighter-rouge">getPort</code> function that we created before:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">port</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./listener'</span><span class="p">).</span><span class="nx">getPort</span><span class="p">;</span>
</code></pre></div></div>
<p>Update the promise chain:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// main</span>
<span class="k">return</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nx">UserName</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addTargetGroup</span><span class="p">(</span><span class="s1">'users'</span><span class="p">,</span> <span class="s1">'3000'</span><span class="p">,</span> <span class="s1">'/users/ping'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">USERS_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Target Group Added!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addTargetGroup</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">,</span> <span class="s1">'3000'</span><span class="p">,</span> <span class="s1">'/movies/ping'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">MOVIES_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Target Group Added!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addTargetGroup</span><span class="p">(</span><span class="s1">'web'</span><span class="p">,</span> <span class="s1">'9000'</span><span class="p">,</span> <span class="s1">'/'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">WEB_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Target Group Added!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addListener</span><span class="p">(</span><span class="nx">MAX_PORT</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">LISTENER_ARN</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">Listeners</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">ListenerArn</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Listener added on port </span><span class="p">${</span><span class="nx">res</span><span class="p">.</span><span class="nx">Listeners</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">Port</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
</code></pre></div></div>
<p>Be sure to add <code class="highlighter-rouge">let LISTENER_ARN;</code> to the top as well.</p>
<h4 id="add-rules">Add rules</h4>
<p>Finally, add some rules to the Listener:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">addRule</span><span class="p">(</span><span class="nx">targetGroup</span><span class="p">,</span> <span class="nx">pattern</span><span class="p">,</span> <span class="nx">listener</span><span class="p">,</span> <span class="nx">priority</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">Actions</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">TargetGroupArn</span><span class="p">:</span> <span class="nx">targetGroup</span><span class="p">,</span>
<span class="na">Type</span><span class="p">:</span> <span class="s1">'forward'</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">Conditions</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">Field</span><span class="p">:</span> <span class="s1">'path-pattern'</span><span class="p">,</span>
<span class="na">Values</span><span class="p">:</span> <span class="p">[</span><span class="nx">pattern</span><span class="p">]</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">ListenerArn</span><span class="p">:</span> <span class="nx">listener</span><span class="p">,</span>
<span class="na">Priority</span><span class="p">:</span> <span class="nx">priority</span>
<span class="p">};</span>
<span class="nx">elbv2</span><span class="p">.</span><span class="nx">createRule</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Update the promise chain again:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// main</span>
<span class="k">return</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nx">UserName</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addTargetGroup</span><span class="p">(</span><span class="s1">'users'</span><span class="p">,</span> <span class="s1">'3000'</span><span class="p">,</span> <span class="s1">'/users/ping'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">USERS_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Target Group Added!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addTargetGroup</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">,</span> <span class="s1">'3000'</span><span class="p">,</span> <span class="s1">'/movies/ping'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">MOVIES_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Target Group Added!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addTargetGroup</span><span class="p">(</span><span class="s1">'web'</span><span class="p">,</span> <span class="s1">'9000'</span><span class="p">,</span> <span class="s1">'/'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">WEB_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Target Group Added!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">port</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">port</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">addListener</span><span class="p">(</span><span class="nx">port</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">LISTENER_ARN</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">Listeners</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">ListenerArn</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Listener added on port </span><span class="p">${</span><span class="nx">res</span><span class="p">.</span><span class="nx">Listeners</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">Port</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addRule</span><span class="p">(</span><span class="nx">USERS_TARGET_GROUP_ARN</span><span class="p">,</span> <span class="s1">'/users*'</span><span class="p">,</span> <span class="nx">LISTENER_ARN</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Rule Added!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addRule</span><span class="p">(</span><span class="nx">MOVIES_TARGET_GROUP_ARN</span><span class="p">,</span> <span class="s1">'/movies*'</span><span class="p">,</span> <span class="nx">LISTENER_ARN</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Rule Added!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addRule</span><span class="p">(</span><span class="nx">MOVIES_TARGET_GROUP_ARN</span><span class="p">,</span> <span class="s1">'/docs*'</span><span class="p">,</span> <span class="nx">LISTENER_ARN</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Rule Added!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addRule</span><span class="p">(</span><span class="nx">WEB_TARGET_GROUP_ARN</span><span class="p">,</span> <span class="s1">'/*'</span><span class="p">,</span> <span class="nx">LISTENER_ARN</span><span class="p">,</span> <span class="mi">4</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Rule Added!'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
</code></pre></div></div>
<p>Commit and push your changes. After the build passes, make sure the Listener was added to the Application Load Balancer:</p>
<p><a href="/assets/img/blog/on-demand-environments/aws-load-balancer-listeners.png"><img src="/assets/img/blog/on-demand-environments/aws-load-balancer-listeners.png" alt="aws load balancer listeners" /></a></p>
<p>Also, make sure the Listener has four Rules:</p>
<p><a href="/assets/img/blog/on-demand-environments/aws-listeners-rules.png"><img src="/assets/img/blog/on-demand-environments/aws-listeners-rules.png" alt="aws listeners rules" /></a></p>
<h3 id="6-create-a-new-service">(6) Create a new Service</h3>
<p>A <a href="http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_services.html">Service</a> is used to spin up and run a number of Tasks within the ECS Cluster. Since we’ve already pre-configured the Cluster and assigned EC2 instances to it, we can simply create a new Service that uses the Cluster resources.</p>
<p>Start by adding a new file called <em>services.js</em> to “ecs/scripts”:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">AWS</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'aws-sdk'</span><span class="p">);</span>
<span class="c1">// globals</span>
<span class="kd">const</span> <span class="nx">AWS_ACCOUNT_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCOUNT_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_ACCESS_KEY_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_USERNAME</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_USERNAME</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_CONFIG_REGION</span> <span class="o">=</span> <span class="s1">'us-west-2'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">SHORT_GIT_HASH</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CIRCLE_SHA1</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">7</span><span class="p">);</span>
<span class="c1">// config</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">Config</span><span class="p">();</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">accessKeyId</span> <span class="o">=</span> <span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">secretAccessKey</span> <span class="o">=</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">region</span> <span class="o">=</span> <span class="nx">AWS_CONFIG_REGION</span><span class="p">;</span>
<span class="c1">// init aws services</span>
<span class="kd">const</span> <span class="nx">ecs</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">ECS</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">iam</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">IAM</span><span class="p">();</span>
<span class="c1">// methods</span>
<span class="kd">function</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span> <span class="na">UserName</span><span class="p">:</span> <span class="nx">AWS_USERNAME</span> <span class="p">};</span>
<span class="nx">iam</span><span class="p">.</span><span class="nx">getUser</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">addService</span><span class="p">(</span><span class="nx">service</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="nx">service</span><span class="p">;</span>
<span class="nx">ecs</span><span class="p">.</span><span class="nx">createService</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="c1">// main</span>
<span class="k">return</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nx">UserName</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
</code></pre></div></div>
<p>Next, add a new directory to “ecs” called “services”, and then add the following files:</p>
<ol>
<li><em>users-review_service.js</em></li>
<li><em>movies-review_service.js</em></li>
<li><em>web-review_service.js</em></li>
</ol>
<h5 id="users---users-review_servicejs">Users - <em>users-review_service.js</em></h5>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createUsersService</span><span class="p">(</span><span class="nx">cluster</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">targetGroup</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">cluster</span><span class="p">:</span> <span class="nx">cluster</span><span class="p">,</span>
<span class="na">serviceName</span><span class="p">:</span> <span class="nx">name</span><span class="p">,</span>
<span class="na">taskDefinition</span><span class="p">:</span> <span class="s1">'microservicemovies-review-users-td'</span><span class="p">,</span>
<span class="na">loadBalancers</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">targetGroupArn</span><span class="p">:</span> <span class="nx">targetGroup</span><span class="p">,</span>
<span class="na">containerName</span><span class="p">:</span> <span class="s2">"users-service-review"</span><span class="p">,</span>
<span class="na">containerPort</span><span class="p">:</span> <span class="mi">3000</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">desiredCount</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="na">role</span><span class="p">:</span> <span class="s2">"ecsServiceRole"</span>
<span class="p">};</span>
<span class="k">return</span> <span class="nx">params</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">createUsersService</span>
<span class="p">};</span>
</code></pre></div></div>
<h5 id="movies---movies-review_servicejs">Movies - <em>movies-review_service.js</em></h5>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createMoviesService</span><span class="p">(</span><span class="nx">cluster</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">targetGroup</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">cluster</span><span class="p">:</span> <span class="nx">cluster</span><span class="p">,</span>
<span class="na">serviceName</span><span class="p">:</span> <span class="nx">name</span><span class="p">,</span>
<span class="na">taskDefinition</span><span class="p">:</span> <span class="s1">'microservicemovies-review-movies-td'</span><span class="p">,</span>
<span class="na">loadBalancers</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">targetGroupArn</span><span class="p">:</span> <span class="nx">targetGroup</span><span class="p">,</span>
<span class="na">containerName</span><span class="p">:</span> <span class="s2">"movies-service-review"</span><span class="p">,</span>
<span class="na">containerPort</span><span class="p">:</span> <span class="mi">3000</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">desiredCount</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="na">role</span><span class="p">:</span> <span class="s2">"ecsServiceRole"</span>
<span class="p">};</span>
<span class="k">return</span> <span class="nx">params</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">createMoviesService</span>
<span class="p">};</span>
</code></pre></div></div>
<h5 id="web---web-review_servicejs">Web - <em>web-review_service.js</em></h5>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createWebService</span><span class="p">(</span><span class="nx">cluster</span><span class="p">,</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">targetGroup</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">cluster</span><span class="p">:</span> <span class="nx">cluster</span><span class="p">,</span>
<span class="na">serviceName</span><span class="p">:</span> <span class="nx">name</span><span class="p">,</span>
<span class="na">taskDefinition</span><span class="p">:</span> <span class="s1">'microservicemovies-review-web-td'</span><span class="p">,</span>
<span class="na">loadBalancers</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">targetGroupArn</span><span class="p">:</span> <span class="nx">targetGroup</span><span class="p">,</span>
<span class="na">containerName</span><span class="p">:</span> <span class="s2">"web-service-review"</span><span class="p">,</span>
<span class="na">containerPort</span><span class="p">:</span> <span class="mi">9000</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="na">desiredCount</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="na">role</span><span class="p">:</span> <span class="s2">"ecsServiceRole"</span>
<span class="p">};</span>
<span class="k">return</span> <span class="nx">params</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">createWebService</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Update <em>services.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">AWS</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'aws-sdk'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">createUsersService</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../services/users-review_service'</span><span class="p">).</span><span class="nx">createUsersService</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">createMoviesService</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../services/movies-review_service'</span><span class="p">).</span><span class="nx">createMoviesService</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">createWebService</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../services/web-review_service'</span><span class="p">).</span><span class="nx">createWebService</span><span class="p">;</span>
<span class="c1">// globals</span>
<span class="kd">const</span> <span class="nx">AWS_ACCOUNT_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCOUNT_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_ACCESS_KEY_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_USERNAME</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_USERNAME</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_CONFIG_REGION</span> <span class="o">=</span> <span class="s1">'us-west-2'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">SHORT_GIT_HASH</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CIRCLE_SHA1</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">7</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">LOAD_BALANCER_ARN</span> <span class="o">=</span> <span class="s1">'arn:aws:elasticloadbalancing:us-west-2:046505967931:loadbalancer/app/microservicemovies-review/493be740ee6aea54'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">CLUSTER_NAME</span> <span class="o">=</span> <span class="s1">'microservicemovies-review'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">USERS_TARGET_GROUP_ARN</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">MOVIES_TARGET_GROUP_ARN</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">WEB_TARGET_GROUP_ARN</span><span class="p">;</span>
<span class="c1">// config</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">Config</span><span class="p">();</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">accessKeyId</span> <span class="o">=</span> <span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">secretAccessKey</span> <span class="o">=</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">region</span> <span class="o">=</span> <span class="nx">AWS_CONFIG_REGION</span><span class="p">;</span>
<span class="c1">// init aws services</span>
<span class="kd">const</span> <span class="nx">ecs</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">ECS</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">iam</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">IAM</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">elbv2</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">ELBv2</span><span class="p">();</span>
<span class="c1">// methods</span>
<span class="kd">function</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span> <span class="na">UserName</span><span class="p">:</span> <span class="nx">AWS_USERNAME</span> <span class="p">};</span>
<span class="nx">iam</span><span class="p">.</span><span class="nx">getUser</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">addService</span><span class="p">(</span><span class="nx">service</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="nx">service</span><span class="p">;</span>
<span class="nx">ecs</span><span class="p">.</span><span class="nx">createService</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">getTargetGroups</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">LoadBalancerArn</span><span class="p">:</span> <span class="nx">LOAD_BALANCER_ARN</span>
<span class="p">};</span>
<span class="nx">elbv2</span><span class="p">.</span><span class="nx">describeTargetGroups</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="c1">// main</span>
<span class="k">return</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nx">UserName</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">getTargetGroups</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">groups</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">group</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">group</span><span class="p">.</span><span class="nx">TargetGroupName</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">SHORT_GIT_HASH</span><span class="p">);</span>
<span class="p">});</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">group</span> <span class="k">of</span> <span class="nx">groups</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">group</span><span class="p">.</span><span class="nx">TargetGroupName</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="s1">'users'</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">USERS_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">group</span><span class="p">.</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">group</span><span class="p">.</span><span class="nx">TargetGroupName</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">MOVIES_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">group</span><span class="p">.</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">group</span><span class="p">.</span><span class="nx">TargetGroupName</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="s1">'web'</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">WEB_TARGET_GROUP_ARN</span> <span class="o">=</span> <span class="nx">group</span><span class="p">.</span><span class="nx">TargetGroupArn</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">userServiceParams</span> <span class="o">=</span> <span class="nx">createUsersService</span><span class="p">(</span>
<span class="nx">CLUSTER_NAME</span><span class="p">,</span> <span class="s2">`</span><span class="p">${</span><span class="nx">SHORT_GIT_HASH</span><span class="p">}</span><span class="s2">-users`</span><span class="p">,</span> <span class="nx">USERS_TARGET_GROUP_ARN</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addService</span><span class="p">(</span><span class="nx">userServiceParams</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Service Added!'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">moviesServiceParams</span> <span class="o">=</span> <span class="nx">createMoviesService</span><span class="p">(</span>
<span class="nx">CLUSTER_NAME</span><span class="p">,</span> <span class="s2">`</span><span class="p">${</span><span class="nx">SHORT_GIT_HASH</span><span class="p">}</span><span class="s2">-movies`</span><span class="p">,</span> <span class="nx">MOVIES_TARGET_GROUP_ARN</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addService</span><span class="p">(</span><span class="nx">moviesServiceParams</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Service Added!'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">webServiceParams</span> <span class="o">=</span> <span class="nx">createWebService</span><span class="p">(</span>
<span class="nx">CLUSTER_NAME</span><span class="p">,</span> <span class="s2">`</span><span class="p">${</span><span class="nx">SHORT_GIT_HASH</span><span class="p">}</span><span class="s2">-web`</span><span class="p">,</span> <span class="nx">WEB_TARGET_GROUP_ARN</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">addService</span><span class="p">(</span><span class="nx">webServiceParams</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Service Added!'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
</code></pre></div></div>
<p>Update the <code class="highlighter-rouge">Deploy</code> command in <em>.circle/config.yml</em>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">run</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Deploy</span>
<span class="na">command</span><span class="pi">:</span> <span class="pi">|</span>
<span class="no">npm install</span>
<span class="no">node ecs/scripts/setup.js</span>
<span class="no">sh ecs/scripts/ecr.sh</span>
<span class="no">node ecs/scripts/tasks.js</span>
<span class="no">node ecs/scripts/alb.js</span>
<span class="no">node ecs/scripts/services.js</span>
</code></pre></div></div>
<p>Again, commit and push to GitHub, and then ensure:</p>
<ol>
<li>The Circle build passes</li>
<li>The Services were created on the <code class="highlighter-rouge">microservicemovies-review</code> Cluster</li>
<li>Each Service has a running Task associated with it</li>
<li>Three Target Groups were created, each with health targets</li>
<li>A Listener was added to the Application Load Balancer with four Rules</li>
<li>Logs were added to <a href="https://console.aws.amazon.com/cloudwatch">CloudWatch</a></li>
<li>The following endpoints work-
<ul>
<li>http://LOAD_BALANCER_DNS:LISTENER_PORT</li>
<li>http://LOAD_BALANCER_DNS:LISTENER_PORT/users/ping</li>
<li>http://LOAD_BALANCER_DNS:LISTENER_PORT/movies/ping</li>
</ul>
</li>
</ol>
<p><a href="/assets/img/blog/on-demand-environments/aws-ecs-services.png"><img src="/assets/img/blog/on-demand-environments/aws-ecs-services.png" alt="aws ecs services" /></a></p>
<h2 id="testing">Testing</h2>
<p>To run the end-to-end tests, first change each of the URLs in <em>tests/sample.test.js</em> from <code class="highlighter-rouge">http://localhost:3007</code> to <code class="highlighter-rouge">http://LOAD_BALANCER_DNS:LISTENER_PORT</code>, and then run the tests locally:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>testcafe firefox tests/<span class="k">**</span>/<span class="k">*</span>.js
</code></pre></div></div>
<p>They should pass.</p>
<h2 id="teardown">Teardown</h2>
<p>Next, let’s add a script to handle the tearing down of the AWS resources after testing is complete.</p>
<p>Main Steps:</p>
<ol>
<li>Remove Listener</li>
<li>Remove Target Groups</li>
<li>Remove Services</li>
</ol>
<p>Start by adding a new script called <em>teardown.js</em> to “ecs/scripts”.</p>
<h3 id="1-remove-listener">(1) Remove Listener</h3>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">AWS</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'aws-sdk'</span><span class="p">);</span>
<span class="c1">// globals</span>
<span class="kd">const</span> <span class="nx">AWS_ACCOUNT_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCOUNT_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_ACCESS_KEY_ID</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_USERNAME</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">AWS_USERNAME</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">AWS_CONFIG_REGION</span> <span class="o">=</span> <span class="s1">'us-west-2'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">LOAD_BALANCER_ARN</span> <span class="o">=</span> <span class="s1">'UPDATE_ME'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">ARGS</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">argv</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">USAGE_MESSAGE</span> <span class="o">=</span> <span class="s1">'</span><span class="err">\</span><span class="s1">nusage:</span><span class="err">\</span><span class="s1">n teardown.js LISTENER_PORT COMMIT_SHA</span><span class="err">\</span><span class="s1">n'</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ARGS</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">||</span> <span class="o">!</span><span class="nx">ARGS</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">USAGE_MESSAGE</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">SHORT_GIT_HASH</span> <span class="o">=</span> <span class="nx">ARGS</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">7</span><span class="p">);</span>
<span class="c1">// config</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">Config</span><span class="p">();</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">accessKeyId</span> <span class="o">=</span> <span class="nx">AWS_ACCESS_KEY_ID</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">secretAccessKey</span> <span class="o">=</span> <span class="nx">AWS_SECRET_ACCESS_KEY</span><span class="p">;</span>
<span class="nx">AWS</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">region</span> <span class="o">=</span> <span class="nx">AWS_CONFIG_REGION</span><span class="p">;</span>
<span class="c1">// init aws services</span>
<span class="kd">const</span> <span class="nx">iam</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">IAM</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">elbv2</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">ELBv2</span><span class="p">();</span>
<span class="c1">// methods</span>
<span class="kd">function</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span> <span class="na">UserName</span><span class="p">:</span> <span class="nx">AWS_USERNAME</span> <span class="p">};</span>
<span class="nx">iam</span><span class="p">.</span><span class="nx">getUser</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">getListeners</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">LoadBalancerArn</span><span class="p">:</span> <span class="nx">LOAD_BALANCER_ARN</span>
<span class="p">};</span>
<span class="nx">elbv2</span><span class="p">.</span><span class="nx">describeListeners</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">removeListener</span><span class="p">(</span><span class="nx">listener</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">ListenerArn</span><span class="p">:</span> <span class="nx">listener</span>
<span class="p">};</span>
<span class="nx">elbv2</span><span class="p">.</span><span class="nx">deleteListener</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="c1">// main</span>
<span class="k">return</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nx">UserName</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">getListeners</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">listener</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">Listeners</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">listener</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">listener</span><span class="p">.</span><span class="nx">Port</span><span class="p">)</span> <span class="o">===</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">ARGS</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
<span class="p">})[</span><span class="mi">0</span><span class="p">];</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">listener</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'Listener does not exist.'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">removeListener</span><span class="p">(</span><span class="nx">listener</span><span class="p">.</span><span class="nx">ListenerArn</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Listener Removed!'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
</code></pre></div></div>
<p>Here, we grabbed all Listeners in <code class="highlighter-rouge">getListeners</code> and then filtered them by the provided listener port. From there, we deleted the listener. Be sure to update <code class="highlighter-rouge">LOAD_BALANCER_ARN</code> before continuing.</p>
<p>Test this out:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>node ecs/scripts/teardown.js LISTENER_PORT COMMIT_SHA
</code></pre></div></div>
<h3 id="2-remove-target-groups">(2) Remove Target Groups</h3>
<p>Add the following two functions to <em>teardown.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">getTargetGroups</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nx">elbv2</span><span class="p">.</span><span class="nx">describeTargetGroups</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">removeTargetGroup</span><span class="p">(</span><span class="nx">targetgroup</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span> <span class="na">TargetGroupArn</span><span class="p">:</span> <span class="nx">targetgroup</span> <span class="p">};</span>
<span class="nx">elbv2</span><span class="p">.</span><span class="nx">deleteTargetGroup</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Then update the promise chain:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// main</span>
<span class="k">return</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nx">UserName</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">getListeners</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">listener</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">Listeners</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">listener</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">listener</span><span class="p">.</span><span class="nx">Port</span><span class="p">)</span> <span class="o">===</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">ARGS</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
<span class="p">})[</span><span class="mi">0</span><span class="p">];</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">listener</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'Listener does not exist.'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">removeListener</span><span class="p">(</span><span class="nx">listener</span><span class="p">.</span><span class="nx">ListenerArn</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Listener Removed!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">getTargetGroups</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">targets</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">group</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">group</span><span class="p">.</span><span class="nx">TargetGroupName</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">SHORT_GIT_HASH</span><span class="p">);</span>
<span class="p">});</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">targets</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'Targets do not exist.'</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">promises</span> <span class="o">=</span> <span class="nx">targets</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">target</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">removeTargetGroup</span><span class="p">(</span><span class="nx">target</span><span class="p">.</span><span class="nx">TargetGroupArn</span><span class="p">);</span>
<span class="p">});</span>
<span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span><span class="nx">promises</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Target Groups Removed!'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
</code></pre></div></div>
<h3 id="3-remove-services">(3) Remove Services</h3>
<p>Finally, add the following functions to remove the Services:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">updateServiceCount</span><span class="p">(</span><span class="nx">serviceName</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">service</span><span class="p">:</span> <span class="nx">serviceName</span><span class="p">,</span>
<span class="na">desiredCount</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="na">cluster</span><span class="p">:</span> <span class="nx">CLUSTER_NAME</span>
<span class="p">};</span>
<span class="nx">ecs</span><span class="p">.</span><span class="nx">updateService</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">removeService</span><span class="p">(</span><span class="nx">serviceName</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">service</span><span class="p">:</span> <span class="nx">serviceName</span><span class="p">,</span>
<span class="na">cluster</span><span class="p">:</span> <span class="nx">CLUSTER_NAME</span>
<span class="p">};</span>
<span class="nx">ecs</span><span class="p">.</span><span class="nx">deleteService</span><span class="p">(</span><span class="nx">params</span><span class="p">,</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reject</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">}</span>
<span class="nx">resolve</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Add the variables to the top:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">CLUSTER_NAME</span> <span class="o">=</span> <span class="s1">'microservicemovies-review'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">ecs</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AWS</span><span class="p">.</span><span class="nx">ECS</span><span class="p">();</span>
</code></pre></div></div>
<p>Then, update the promise chain:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// main</span>
<span class="k">return</span> <span class="nx">ensureAuthenticated</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Welcome </span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nx">UserName</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">getListeners</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">listener</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">Listeners</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">listener</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">listener</span><span class="p">.</span><span class="nx">Port</span><span class="p">)</span> <span class="o">===</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">ARGS</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
<span class="p">})[</span><span class="mi">0</span><span class="p">];</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">listener</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'Listener does not exist.'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">removeListener</span><span class="p">(</span><span class="nx">listener</span><span class="p">.</span><span class="nx">ListenerArn</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Listener Removed!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">getTargetGroups</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">targets</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">TargetGroups</span><span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">group</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">group</span><span class="p">.</span><span class="nx">TargetGroupName</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">SHORT_GIT_HASH</span><span class="p">);</span>
<span class="p">});</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">targets</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">'Targets do not exist.'</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">promises</span> <span class="o">=</span> <span class="nx">targets</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">target</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">removeTargetGroup</span><span class="p">(</span><span class="nx">target</span><span class="p">.</span><span class="nx">TargetGroupArn</span><span class="p">);</span>
<span class="p">});</span>
<span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span><span class="nx">promises</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Target Groups Removed!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">updateServiceCount</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">SHORT_GIT_HASH</span><span class="p">}</span><span class="s2">-users`</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Service Updated!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">removeService</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">SHORT_GIT_HASH</span><span class="p">}</span><span class="s2">-users`</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Service Removed!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">updateServiceCount</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">SHORT_GIT_HASH</span><span class="p">}</span><span class="s2">-movies`</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Service Updated!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">removeService</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">SHORT_GIT_HASH</span><span class="p">}</span><span class="s2">-movies`</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Service Removed!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">updateServiceCount</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">SHORT_GIT_HASH</span><span class="p">}</span><span class="s2">-web`</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Service Updated!'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">removeService</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">SHORT_GIT_HASH</span><span class="p">}</span><span class="s2">-web`</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">'Service removed!'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
</code></pre></div></div>
<p>Test it out again:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>node ecs/scripts/teardown.js LISTENER_PORT COMMIT_SHA
</code></pre></div></div>
<p>You should see:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Welcome AWS_USERNAME!
Listener Removed!
Target Groups Removed!
Service Updated!
Service Removed!
Service Updated!
Service Removed!
Service Updated!
Service removed!
</code></pre></div></div>
<p>Make sure all associated AWS Resources were removed as well.</p>
<h2 id="conclusion-and-next-steps">Conclusion and Next Steps</h2>
<p>That’s it!</p>
<h3 id="development-workflow-1">Development Workflow</h3>
<p>Let’s quickly review the development workflow…</p>
<ol>
<li>Local development
<ul>
<li>Create a new feature branch from the master branch</li>
<li>Make code changes</li>
<li>Commit and push code to GitHub</li>
</ul>
</li>
<li>Continuous integration
<ul>
<li>Open a new PR against the development branch</li>
<li>A new build is then triggered on Circle CI</li>
<li>If the build passes, manually merge the PR</li>
<li>A new build is triggered again on Circle CI</li>
<li>If the build passes, deployment occurs…</li>
</ul>
</li>
<li>Deployment on AWS via deployment scripts</li>
<li>Testing
<ul>
<li>Update then run the end-to-end tests</li>
</ul>
</li>
<li>Teardown
<ul>
<li>Run the teardown script</li>
</ul>
</li>
</ol>
<h3 id="whats-next">What’s next?</h3>
<p>Did you test out the Swagger docs? They don’t work. What’s happening? Fix this on your own.</p>
<p>Developers will inevitably forget to run the teardown script. Configure an <a href="https://aws.amazon.com/lambda/">AWS Lambda</a> function to run nightly to tear down all AWS resources associated with the on-demand test environments.</p>
<p>Add <a href="https://www.vaultproject.io/">Vault</a> and <a href="https://www.consul.io/">Consul</a> into the mix to handle secrets and environment variables.</p>
<p>Grab the final code from the <a href="https://github.com/mjhea0/microservice-movies/releases/tag/v4">v4</a> tag of the <a href="https://github.com/mjhea0/microservice-movies">microservice-movies</a> repo. Please add questions and/or comments below. Cheers!</p>
Michael Herman
In this tutorial, we’ll look at how to spin up reproducible (and easily-destructible), on-demand test environments with Docker, Amazon EC2 Container Service (ECS), and Circle CI (for continuous integration and delivery).
Building a RESTful API with Koa and Postgres
2017-08-23T00:00:00-05:00
2017-08-23T00:00:00-05:00
https://mherman.org/blog/building-a-restful-api-with-koa-and-postgres
<p>In this tutorial, you’ll learn how to develop a RESTful API with <a href="http://koajs.com/">Koa</a> 2 and Postgres. You’ll also be taking advantage of <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function">async/await</a> functions, from ES2017, and test driven development (TDD).</p>
<blockquote>
<p>This tutorial requires Node v<a href="https://nodejs.org/en/blog/release/v7.6.0/">7.6.0</a> or greater.</p>
</blockquote>
<div style="text-align:center;">
<img src="/assets/img/blog/node-koa-api.png" style="max-width: 90%; border:0; box-shadow: none;" alt="node koa" />
</div>
<p><br /></p>
<h4 id="parts">Parts</h4>
<p>This article is part of a 4-part Koa and Sinon series…</p>
<ol>
<li><a href="http://mherman.org/blog/2017/08/23/building-a-restful-api-with-koa-and-postgres">Building a RESTful API with Koa and Postgres</a> (this article)</li>
<li><a href="http://mherman.org/blog/2017/11/06/stubbing-http-requests-with-sinon">Stubbing HTTP Requests with Sinon</a></li>
<li><a href="http://mherman.org/blog/2018/01/02/user-authentication-with-passport-and-koa">User Authentication with Passport and Koa</a></li>
<li><a href="http://mherman.org/blog/2018/01/22/stubbing-node-authentication-middleware-with-sinon">Stubbing Node Authentication Middleware with Sinon</a></li>
</ol>
<h4 id="npm-dependencies">NPM Dependencies</h4>
<ol>
<li>Koa v<a href="https://github.com/koajs/koa/releases/tag/2.3.0">2.3.0</a></li>
<li>Mocha v<a href="https://github.com/mochajs/mocha/releases/tag/v3.5.0">3.5.0</a></li>
<li>Chai v<a href="https://github.com/chaijs/chai/releases/tag/4.1.1">4.1.1</a></li>
<li>Chai HTTP v<a href="https://github.com/chaijs/chai-http/releases/tag/3.0.0">3.0.0</a></li>
<li>Knex v<a href="https://github.com/tgriesser/knex/releases/tag/0.13.0">0.13.0</a></li>
<li>pg v<a href="https://github.com/brianc/node-postgres/releases/tag/v7.1.2">7.1.2</a></li>
<li>koa-router v<a href="https://github.com/alexmingoia/koa-router/releases/tag/v7.2.1">7.2.1</a></li>
<li>koa-bodyparser v<a href="https://github.com/koajs/bodyparser/releases/tag/4.2.0">4.2.0</a></li>
</ol>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#getting-started" id="markdown-toc-getting-started">Getting Started</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#routes" id="markdown-toc-routes">Routes</a></li>
<li><a href="#next-steps" id="markdown-toc-next-steps">Next Steps</a></li>
</ul>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you will be able to…</p>
<ol>
<li>Set up a project with Koa using test driven development</li>
<li>Write schema migration files with Knex to create new database tables</li>
<li>Generate database seed files with Knex and apply the seeds to the database</li>
<li>Set up the testing structure with Mocha and Chai</li>
<li>Perform the basic CRUD functions on a RESTful resource with Knex methods</li>
<li>Create a CRUD app, following RESTful best practices</li>
<li>Write integration tests</li>
<li>Write tests, and then write just enough code to pass the tests</li>
<li>Create routes with Koa Router</li>
<li>Parse the request body with koa-bodyparser</li>
</ol>
<h2 id="getting-started">Getting Started</h2>
<h3 id="what-are-we-building">What are we building?</h3>
<p>Your goal is to design a RESTful API, using test driven development, for a single resource - <code class="highlighter-rouge">movies</code>. The API itself should follow RESTful design principles, using the <a href="http://www.restapitutorial.com/lessons/httpmethods.html">basic HTTP verbs</a>: GET, POST, PUT, and DELETE.</p>
<h3 id="what-is-koa">What is Koa?</h3>
<p>Koa is a web framework for Node.js.</p>
<p>Although it’s designed by the same team that created Express, it’s much lighter than Express though - so it comes with very little out of the box. It’s really just a tiny wrapper on top of Node’s <a href="https://nodejs.org/api/http.html#http_http">HTTP</a> module. Koa allows you - the developer - to pick and choose the tools you want to use from the <a href="https://github.com/koajs/koa/wiki">community</a>.</p>
<p>It has native support for <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function">async/await</a>, which makes it easier and faster to develop an API since you don’t have to deal with <a href="https://en.wikipedia.org/wiki/Callback_(computer_programming)">callbacks</a> and <a href="http://callbackhell.com/">callback hell</a>.</p>
<p>Finally, since Koa has similar patterns to Express, it’s relatively easy to pick up if you’ve worked at all with Express.</p>
<blockquote>
<p><strong>NOTE</strong>: For more, review <a href="https://github.com/koajs/koa/blob/master/docs/koa-vs-express.md">Koa vs Express</a>.</p>
</blockquote>
<h3 id="tdd">TDD</h3>
<p>Test Driven Development (TDD) is an iterative development cycle that emphasizes writing automated tests <em>before</em> writing the actual code.</p>
<h4 id="why">Why?</h4>
<ol>
<li>Helps break down problems into manageable pieces since you should have a better understanding of what you’re going to write</li>
<li>Forces you to write cleaner code</li>
<li>Prevents over coding</li>
</ol>
<h4 id="red-green-refactor">Red-Green-Refactor</h4>
<p>TDD often follows the “Red-Green-Refactor” development cycle:</p>
<ol>
<li><span style="color:red">RED</span>: Write a test, which should fail when you run it</li>
<li><span style="color:green">GREEN</span>: Write just enough code for the test to pass</li>
<li><span style="color:orange">REFACTOR</span>: Refactor code and retest, again and again (if necessary)</li>
</ol>
<h2 id="project-setup">Project Setup</h2>
<p>Start by cloning down the base project:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/mjhea0/node-koa-api <span class="se">\</span>
<span class="nt">--branch</span> v1 <span class="nt">--single-branch</span>
<span class="nv">$ </span><span class="nb">cd </span>node-koa-api
</code></pre></div></div>
<p>Then, check out the <a href="https://github.com/mjhea0/node-koa-api/releases/tag/v1">v1</a> tag to the master branch and install the dependencies:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git checkout tags/v1 <span class="nt">-b</span> master
<span class="nv">$ </span>npm install
</code></pre></div></div>
<p>Run two quick sanity checks to make sure all is well:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm start
It works!
<span class="nv">$ </span>npm <span class="nb">test
</span>Sample Test
✓ should pass
1 passing <span class="o">(</span>8ms<span class="o">)</span>
</code></pre></div></div>
<p>Take a quick look at the project structure before moving on.</p>
<h3 id="koa">Koa</h3>
<p>As always, we’ll begin with the obligatory hello world. But first, since we’re following TDD, let’s write a quick test.</p>
<p>Install <a href="https://github.com/chaijs/chai-http">Chai HTTP</a> so we can test HTTP calls:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install chai-http@3.0.0 <span class="nt">--save-dev</span>
</code></pre></div></div>
<p>Create a new file in the “test” directory called <em>routes.index.test.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">=</span> <span class="s1">'test'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">chai</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'chai'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">should</span> <span class="o">=</span> <span class="nx">chai</span><span class="p">.</span><span class="nx">should</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">chaiHttp</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'chai-http'</span><span class="p">);</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">chaiHttp</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../src/server/index'</span><span class="p">);</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'routes : index'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should return json'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">message</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'hello, world!'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>So, within the second <code class="highlighter-rouge">describe</code> block, we have a single <code class="highlighter-rouge">it</code> statement, which defines a test case. In this simple case, we’re testing the response from a GET request to the main route, <code class="highlighter-rouge">/</code>.</p>
<p>Run the test via <code class="highlighter-rouge">npm test</code>. You should see the following error since the server is not setup:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TypeError: app.address is not a <span class="k">function</span>
</code></pre></div></div>
<p>Next, let’s stand up a quick Koa server. Install Koa:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install koa@2.3.0 <span class="nt">--save</span>
</code></pre></div></div>
<p>Then, update <em>src/server/index.js</em> like so:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Koa</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Koa</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">PORT</span> <span class="o">=</span> <span class="mi">1337</span><span class="p">;</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span><span class="p">,</span>
<span class="na">message</span><span class="p">:</span> <span class="s1">'hello, world!'</span>
<span class="p">};</span>
<span class="p">});</span>
<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">PORT</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Server listening on port: </span><span class="p">${</span><span class="nx">PORT</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">server</span><span class="p">;</span>
</code></pre></div></div>
<p>Here, we created a new instance of Koa and then mounted a basic async function to the app. This function takes the Koa <a href="http://koajs.com/#context">context</a> as a parameter, <code class="highlighter-rouge">ctx</code>. It’s worth noting that this object encapsulates both the Node request and response objects. We then set the return value to <code class="highlighter-rouge">ctx.body</code>, which will be sent back as the response body when user hits any route.</p>
<p>Run the Koa server, via <code class="highlighter-rouge">npm start</code>, and then navigate to <a href="http://localhost:1337/">http://localhost:1337/</a>. You should see:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w">
</span><span class="s2">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hello, world!"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Once done, kill the server and then run the tests. They should now pass.</p>
<h3 id="database">Database</h3>
<p>Moving right along, <a href="https://www.postgresql.org/download/">download</a> and install Postgres, if you don’t already have it, and then fire up the server on port 5432.</p>
<p>Along with Postgres, we’ll use <a href="https://node-postgres.com/">pg</a> and <a href="http://knexjs.org/">Knex</a> to interact with the database itself:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install pg@7.1.2 knex@0.13.0 <span class="nt">--save</span>
</code></pre></div></div>
<p>Install Knex globally as well so you can use the CLI tool:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install knex@0.13.0 <span class="nt">-g</span>
</code></pre></div></div>
<p>Next, we need to create two new databases, one for our development environment and the other for test environment.</p>
<p>Open psql in the terminal, and create the databases:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>psql
psql <span class="o">(</span>9.6.1<span class="o">)</span>
<span class="c"># CREATE DATABASE koa_api;</span>
CREATE DATABASE
<span class="c"># CREATE DATABASE koa_api_test;</span>
CREATE DATABASE
<span class="c"># \q</span>
</code></pre></div></div>
<p>With that, we can now initialize Knex.</p>
<h3 id="knex">Knex</h3>
<p>Run <code class="highlighter-rouge">knex init</code> in the project root to initialize a new <a href="http://knexjs.org/#knexfile">config file</a> called <em>knexfile.js</em>. Override the default info with:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">path</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'path'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">BASE_PATH</span> <span class="o">=</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">,</span> <span class="s1">'src'</span><span class="p">,</span> <span class="s1">'server'</span><span class="p">,</span> <span class="s1">'db'</span><span class="p">);</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">test</span><span class="p">:</span> <span class="p">{</span>
<span class="na">client</span><span class="p">:</span> <span class="s1">'pg'</span><span class="p">,</span>
<span class="na">connection</span><span class="p">:</span> <span class="s1">'postgres://username:password@localhost:5432/koa_api_test'</span><span class="p">,</span>
<span class="na">migrations</span><span class="p">:</span> <span class="p">{</span>
<span class="na">directory</span><span class="p">:</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">BASE_PATH</span><span class="p">,</span> <span class="s1">'migrations'</span><span class="p">)</span>
<span class="p">},</span>
<span class="na">seeds</span><span class="p">:</span> <span class="p">{</span>
<span class="na">directory</span><span class="p">:</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">BASE_PATH</span><span class="p">,</span> <span class="s1">'seeds'</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="na">development</span><span class="p">:</span> <span class="p">{</span>
<span class="na">client</span><span class="p">:</span> <span class="s1">'pg'</span><span class="p">,</span>
<span class="na">connection</span><span class="p">:</span> <span class="s1">'postgres://username:password@localhost:5432/koa_api'</span><span class="p">,</span>
<span class="na">migrations</span><span class="p">:</span> <span class="p">{</span>
<span class="na">directory</span><span class="p">:</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">BASE_PATH</span><span class="p">,</span> <span class="s1">'migrations'</span><span class="p">)</span>
<span class="p">},</span>
<span class="na">seeds</span><span class="p">:</span> <span class="p">{</span>
<span class="na">directory</span><span class="p">:</span> <span class="nx">path</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="nx">BASE_PATH</span><span class="p">,</span> <span class="s1">'seeds'</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>
<blockquote>
<p><strong>NOTE:</strong> Make sure to replace <code class="highlighter-rouge">username</code> and <code class="highlighter-rouge">password</code> with your database username and password, respectively.</p>
</blockquote>
<p>Next, let’s create a new migration to define the database schema:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knex migrate:make movies
</code></pre></div></div>
<p>This created a “src/server/db/migrations” folder with a timestamped migration file. Update the file like so:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">exports</span><span class="p">.</span><span class="nx">up</span> <span class="o">=</span> <span class="p">(</span><span class="nx">knex</span><span class="p">,</span> <span class="nb">Promise</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">createTable</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">,</span> <span class="p">(</span><span class="nx">table</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">table</span><span class="p">.</span><span class="nx">increments</span><span class="p">();</span>
<span class="nx">table</span><span class="p">.</span><span class="nx">string</span><span class="p">(</span><span class="s1">'name'</span><span class="p">).</span><span class="nx">notNullable</span><span class="p">().</span><span class="nx">unique</span><span class="p">();</span>
<span class="nx">table</span><span class="p">.</span><span class="nx">string</span><span class="p">(</span><span class="s1">'genre'</span><span class="p">).</span><span class="nx">notNullable</span><span class="p">();</span>
<span class="nx">table</span><span class="p">.</span><span class="nx">integer</span><span class="p">(</span><span class="s1">'rating'</span><span class="p">).</span><span class="nx">notNullable</span><span class="p">();</span>
<span class="nx">table</span><span class="p">.</span><span class="kr">boolean</span><span class="p">(</span><span class="s1">'explicit'</span><span class="p">).</span><span class="nx">notNullable</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="nx">exports</span><span class="p">.</span><span class="nx">down</span> <span class="o">=</span> <span class="p">(</span><span class="nx">knex</span><span class="p">,</span> <span class="nb">Promise</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">dropTable</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Add a new file to the “db” folder called <em>connection.js</em> to, well, connect to the database using the appropriate Knex configuration based on the environment (development, test, staging, production, etc.):</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">environment</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">||</span> <span class="s1">'development'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">config</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../../../knexfile.js'</span><span class="p">)[</span><span class="nx">environment</span><span class="p">];</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'knex'</span><span class="p">)(</span><span class="nx">config</span><span class="p">);</span>
</code></pre></div></div>
<p>Apply the migration to the development database:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knex migrate:latest <span class="nt">--env</span> development
</code></pre></div></div>
<p>Next, let’s create a seed file to populate the database with some initial data:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knex seed:make movies_seed
</code></pre></div></div>
<p>This added a seed file to “src/server/db/seeds”; update it to match the database schema:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">exports</span><span class="p">.</span><span class="nx">seed</span> <span class="o">=</span> <span class="p">(</span><span class="nx">knex</span><span class="p">,</span> <span class="nb">Promise</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">).</span><span class="nx">del</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">).</span><span class="nx">insert</span><span class="p">({</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'The Land Before Time'</span><span class="p">,</span>
<span class="na">genre</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,</span>
<span class="na">rating</span><span class="p">:</span> <span class="mi">7</span><span class="p">,</span>
<span class="na">explicit</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">});</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">).</span><span class="nx">insert</span><span class="p">({</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'Jurassic Park'</span><span class="p">,</span>
<span class="na">genre</span><span class="p">:</span> <span class="s1">'Science Fiction'</span><span class="p">,</span>
<span class="na">rating</span><span class="p">:</span> <span class="mi">9</span><span class="p">,</span>
<span class="na">explicit</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">});</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">).</span><span class="nx">insert</span><span class="p">({</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'Ice Age: Dawn of the Dinosaurs'</span><span class="p">,</span>
<span class="na">genre</span><span class="p">:</span> <span class="s1">'Action/Romance'</span><span class="p">,</span>
<span class="na">rating</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
<span class="na">explicit</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Apply the seed:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knex seed:run <span class="nt">--env</span> development
</code></pre></div></div>
<p>Finally, hop back into psql to ensure the database has been updated:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>psql
psql <span class="o">(</span>9.6.1<span class="o">)</span>
<span class="c"># \c koa_api</span>
You are now connected to database <span class="s2">"koa_api"</span><span class="nb">.</span>
<span class="c"># select * from movies;</span>
id | name | genre | rating | explicit
<span class="nt">----</span>+--------------------------------+-----------------+--------+----------
1 | The Land Before Time | Fantasy | 7 | f
2 | Jurassic Park | Science Fiction | 9 | t
3 | Ice Age: Dawn of the Dinosaurs | Action/Romance | 5 | f
<span class="o">(</span>3 rows<span class="o">)</span>
<span class="c"># \q</span>
</code></pre></div></div>
<h3 id="koa-router">Koa Router</h3>
<p>Unlike Express, Koa does not provide any routing middleware. There are a number of options available, but we’ll use <a href="https://github.com/alexmingoia/koa-router">koa-router</a> due to its simplicity.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install koa-router@7.2.1 <span class="nt">--save</span>
</code></pre></div></div>
<p>Create a new folder called “routes” within “server”, and then add an <em>index.js</em> file to it:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Router</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-router'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">router</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Router</span><span class="p">();</span>
<span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span><span class="p">,</span>
<span class="na">message</span><span class="p">:</span> <span class="s1">'hello, world!'</span>
<span class="p">};</span>
<span class="p">})</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">router</span><span class="p">;</span>
</code></pre></div></div>
<p>Then, update <em>src/server/index.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Koa</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">indexRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./routes/index'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Koa</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">PORT</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span> <span class="o">||</span> <span class="mi">1337</span><span class="p">;</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">indexRoutes</span><span class="p">.</span><span class="nx">routes</span><span class="p">());</span>
<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">PORT</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Server listening on port: </span><span class="p">${</span><span class="nx">PORT</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">server</span><span class="p">;</span>
</code></pre></div></div>
<p>Essentially, we moved the <code class="highlighter-rouge">/</code> route out of the main application file. Ensure the tests still pass before moving on.</p>
<h2 id="routes">Routes</h2>
<p>Again, we’ll take a test-first approach to writing our routes:</p>
<table>
<thead>
<tr>
<th>URL</th>
<th>HTTP Verb</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>/api/v1/movies</td>
<td>GET</td>
<td>Return ALL movies</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>GET</td>
<td>Return a SINGLE movie</td>
</tr>
<tr>
<td>/api/v1/movies</td>
<td>POST</td>
<td>Add a movie</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>PUT</td>
<td>Update a movie</td>
</tr>
<tr>
<td>/api/v1/movies/:id</td>
<td>DELETE</td>
<td>Delete a movie</td>
</tr>
</tbody>
</table>
<p>Before diving in, let’s add some structure. First, add a new folder called “queries” to the “db” folder, and then add a file called <em>movies.js</em> to that newly created folder:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">knex</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../connection'</span><span class="p">);</span>
</code></pre></div></div>
<p>We’ll add the database queries associated with the <code class="highlighter-rouge">movies</code> resource to this file. Next, add a new route file called <em>movies.js</em> to “routes”:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Router</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-router'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">queries</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../db/queries/movies'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">router</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Router</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">BASE_URL</span> <span class="o">=</span> <span class="s2">`/api/v1/movies`</span><span class="p">;</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">router</span><span class="p">;</span>
</code></pre></div></div>
<p>Then, wire this file up to the main application in <em>src/server/index.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Koa</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">indexRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./routes/index'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">movieRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./routes/movies'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Koa</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">PORT</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span> <span class="o">||</span> <span class="mi">1337</span><span class="p">;</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">indexRoutes</span><span class="p">.</span><span class="nx">routes</span><span class="p">());</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">movieRoutes</span><span class="p">.</span><span class="nx">routes</span><span class="p">());</span>
<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">PORT</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Server listening on port: </span><span class="p">${</span><span class="nx">PORT</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">server</span><span class="p">;</span>
</code></pre></div></div>
<p>Finally, add a new test file to “test” called <em>routes.movies.test.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">=</span> <span class="s1">'test'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">chai</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'chai'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">should</span> <span class="o">=</span> <span class="nx">chai</span><span class="p">.</span><span class="nx">should</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">chaiHttp</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'chai-http'</span><span class="p">);</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">chaiHttp</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../src/server/index'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">knex</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../src/server/db/connection'</span><span class="p">);</span>
<span class="nx">describe</span><span class="p">(</span><span class="s1">'routes : movies'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">migrate</span><span class="p">.</span><span class="nx">rollback</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span> <span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">migrate</span><span class="p">.</span><span class="nx">latest</span><span class="p">();</span> <span class="p">})</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span> <span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">seed</span><span class="p">.</span><span class="nx">run</span><span class="p">();</span> <span class="p">});</span>
<span class="p">});</span>
<span class="nx">afterEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">.</span><span class="nx">migrate</span><span class="p">.</span><span class="nx">rollback</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>So, when the tests are ran, the <code class="highlighter-rouge">beforeEach()</code> is fired before any of test specs, applying the migrations to the test database. After the specs run, the database is rolled back to a pristine state in the <code class="highlighter-rouge">afterEach()</code>.</p>
<p>With that, let’s add our routes!</p>
<h3 id="get-all-movies">GET All Movies</h3>
<p>Start with a test:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /api/v1/movies'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should return all movies'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/api/v1/movies'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// there should be no errors</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="c1">// there should be a 200 status code</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="c1">// the response should be JSON</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"status": "success"}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"data": [3 movie objects]}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">length</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
<span class="c1">// the first object in the data array should</span>
<span class="c1">// have the right keys</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Take note of the code comments. Review <a href="http://mherman.org/blog/2015/09/10/testing-node-js-with-mocha-and-chai/">Testing Node.js With Mocha and Chai</a> for more info. Run the test to make sure it fails:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Uncaught AssertionError: expected <span class="o">[</span>Error: Not Found] to not exist
</code></pre></div></div>
<p>To get the test to pass, add the route handler to <em>src/server/routes/movies.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">BASE_URL</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">movies</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">queries</span><span class="p">.</span><span class="nx">getAllMovies</span><span class="p">();</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="nx">movies</span>
<span class="p">};</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>
<p>Add the DB query to <em>src/server/db/queries/movies.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">knex</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'../connection'</span><span class="p">);</span>
<span class="kd">function</span> <span class="nx">getAllMovies</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">select</span><span class="p">(</span><span class="s1">'*'</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">getAllMovies</span>
<span class="p">};</span>
</code></pre></div></div>
<p>So, <code class="highlighter-rouge">getAllMovies()</code> returns a promise object. Then, within the <code class="highlighter-rouge">async</code> function, execution stops at the <code class="highlighter-rouge">await</code>. Execution continues once the promise is resolved.</p>
<p>Run the tests to ensure they pass:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>routes : index
GET /
✓ should <span class="k">return </span>json
routes : movies
GET /api/v1/movies
✓ should <span class="k">return </span>all movies
Sample Test
✓ should pass
3 passing <span class="o">(</span>177ms<span class="o">)</span>
</code></pre></div></div>
<h3 id="get-single-movie">GET Single Movie</h3>
<p>What if we just want a single movie?</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'GET /api/v1/movies/:id'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should respond with a single movie'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/api/v1/movies/1'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// there should be no errors</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="c1">// there should be a 200 status code</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="c1">// the response should be JSON</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"status": "success"}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"data": 1 movie object}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Make sure the test fails, and then add the route handler:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">BASE_URL</span><span class="p">}</span><span class="s2">/:id`</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">movie</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">queries</span><span class="p">.</span><span class="nx">getSingleMovie</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="nx">movie</span>
<span class="p">};</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>
<p>Add the query as well:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">getSingleMovie</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">select</span><span class="p">(</span><span class="s1">'*'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">where</span><span class="p">({</span> <span class="na">id</span><span class="p">:</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span> <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Don’t forget to export it:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="nx">getAllMovies</span><span class="p">,</span>
<span class="nx">getSingleMovie</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>The test should now pass. Before moving on though, what happens if the movie ID does not exist? Start with a test to find out.</p>
<p>Add an <code class="highlighter-rouge">it</code> block to the previous <code class="highlighter-rouge">describe</code> block:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="s1">'should throw an error if the movie does not exist'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s1">'/api/v1/movies/9999999'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// there should an error</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="c1">// there should be a 404 status code</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">404</span><span class="p">);</span>
<span class="c1">// the response should be JSON</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"status": "error"}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'error'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"message": "That movie does not exist."}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">message</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'That movie does not exist.'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Make sure the test fails before updating the code:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">BASE_URL</span><span class="p">}</span><span class="s2">/:id`</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">movie</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">queries</span><span class="p">.</span><span class="nx">getSingleMovie</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">movie</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="nx">movie</span>
<span class="p">};</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">404</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'error'</span><span class="p">,</span>
<span class="na">message</span><span class="p">:</span> <span class="s1">'That movie does not exist.'</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>
<p>The test should now pass.</p>
<h3 id="post">POST</h3>
<p>How about adding a new movie to the database?</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'POST /api/v1/movies'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should return the movie that was added'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'/api/v1/movies'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">send</span><span class="p">({</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'Titanic'</span><span class="p">,</span>
<span class="na">genre</span><span class="p">:</span> <span class="s1">'Drama'</span><span class="p">,</span>
<span class="na">rating</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
<span class="na">explicit</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// there should be no errors</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="c1">// there should be a 201 status code</span>
<span class="c1">// (indicating that something was "created")</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">201</span><span class="p">);</span>
<span class="c1">// the response should be JSON</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"status": "success"}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"data": 1 movie object}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Koa does not parse the request body by default, so we need to add middleware for body parsing. <a href="https://github.com/koajs/bodyparser">koa-bodyparser</a> is a popular choice:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install koa-bodyparser@4.2.0 <span class="nt">--save</span>
</code></pre></div></div>
<p>Add the requirement to <em>src/server/index.js</em>, and then make sure to mount the middleware to the app before the routes:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Koa</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">bodyParser</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'koa-bodyparser'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">indexRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./routes/index'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">movieRoutes</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./routes/movies'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Koa</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">PORT</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span> <span class="o">||</span> <span class="mi">1337</span><span class="p">;</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">bodyParser</span><span class="p">());</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">indexRoutes</span><span class="p">.</span><span class="nx">routes</span><span class="p">());</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">movieRoutes</span><span class="p">.</span><span class="nx">routes</span><span class="p">());</span>
<span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">PORT</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`Server listening on port: </span><span class="p">${</span><span class="nx">PORT</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">server</span><span class="p">;</span>
</code></pre></div></div>
<p>Add the route handler:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">BASE_URL</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">movie</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">queries</span><span class="p">.</span><span class="nx">addMovie</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">movie</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">201</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="nx">movie</span>
<span class="p">};</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">400</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'error'</span><span class="p">,</span>
<span class="na">message</span><span class="p">:</span> <span class="s1">'Something went wrong.'</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>
<p>DB query:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">addMovie</span><span class="p">(</span><span class="nx">movie</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">insert</span><span class="p">(</span><span class="nx">movie</span><span class="p">)</span>
<span class="p">.</span><span class="nx">returning</span><span class="p">(</span><span class="s1">'*'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>What if the payload does not include the correct keys? Add a new <code class="highlighter-rouge">it</code> block:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="s1">'should throw an error if the payload is malformed'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'/api/v1/movies'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">send</span><span class="p">({</span>
<span class="na">name</span><span class="p">:</span> <span class="s1">'Titanic'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// there should an error</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="c1">// there should be a 400 status code</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">400</span><span class="p">);</span>
<span class="c1">// the response should be JSON</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"status": "error"}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'error'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a message key</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Then update the route handler:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">BASE_URL</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">movie</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">queries</span><span class="p">.</span><span class="nx">addMovie</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">movie</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">201</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="nx">movie</span>
<span class="p">};</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">400</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'error'</span><span class="p">,</span>
<span class="na">message</span><span class="p">:</span> <span class="s1">'Something went wrong.'</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">400</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'error'</span><span class="p">,</span>
<span class="na">message</span><span class="p">:</span> <span class="nx">err</span><span class="p">.</span><span class="nx">message</span> <span class="o">||</span> <span class="s1">'Sorry, an error has occurred.'</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>
<h3 id="put">PUT</h3>
<p>Test:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'PUT /api/v1/movies'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should return the movie that was updated'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">select</span><span class="p">(</span><span class="s1">'*'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">movie</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">movieObject</span> <span class="o">=</span> <span class="nx">movie</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="s2">`/api/v1/movies/</span><span class="p">${</span><span class="nx">movieObject</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
<span class="p">.</span><span class="nx">send</span><span class="p">({</span>
<span class="na">rating</span><span class="p">:</span> <span class="mi">9</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// there should be no errors</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="c1">// there should be a 200 status code</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="c1">// the response should be JSON</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"status": "success"}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"data": 1 movie object}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="c1">// ensure the movie was in fact updated</span>
<span class="kd">const</span> <span class="nx">newMovieObject</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
<span class="nx">newMovieObject</span><span class="p">.</span><span class="nx">rating</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="nx">movieObject</span><span class="p">.</span><span class="nx">rating</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Route handler:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">BASE_URL</span><span class="p">}</span><span class="s2">/:id`</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">movie</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">queries</span><span class="p">.</span><span class="nx">updateMovie</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">movie</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">200</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="nx">movie</span>
<span class="p">};</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">404</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'error'</span><span class="p">,</span>
<span class="na">message</span><span class="p">:</span> <span class="s1">'That movie does not exist.'</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">400</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'error'</span><span class="p">,</span>
<span class="na">message</span><span class="p">:</span> <span class="nx">err</span><span class="p">.</span><span class="nx">message</span> <span class="o">||</span> <span class="s1">'Sorry, an error has occurred.'</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>
<p>DB query:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">updateMovie</span><span class="p">(</span><span class="nx">id</span><span class="p">,</span> <span class="nx">movie</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">movie</span><span class="p">)</span>
<span class="p">.</span><span class="nx">where</span><span class="p">({</span> <span class="na">id</span><span class="p">:</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span> <span class="p">})</span>
<span class="p">.</span><span class="nx">returning</span><span class="p">(</span><span class="s1">'*'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Did you notice that we are already handling a case where the movie does not exist in the route handler? Let’s add a test for that:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">it</span><span class="p">(</span><span class="s1">'should throw an error if the movie does not exist'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="s1">'/api/v1/movies/9999999'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">send</span><span class="p">({</span>
<span class="na">rating</span><span class="p">:</span> <span class="mi">9</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// there should an error</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="c1">// there should be a 404 status code</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">404</span><span class="p">);</span>
<span class="c1">// the response should be JSON</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"status": "error"}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'error'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"message": "That movie does not exist."}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">message</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'That movie does not exist.'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<h3 id="delete">DELETE</h3>
<p>Test:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">describe</span><span class="p">(</span><span class="s1">'DELETE /api/v1/movies/:id'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should return the movie that was deleted'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">select</span><span class="p">(</span><span class="s1">'*'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">movies</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">movieObject</span> <span class="o">=</span> <span class="nx">movies</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
<span class="kd">const</span> <span class="nx">lengthBeforeDelete</span> <span class="o">=</span> <span class="nx">movies</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="s2">`/api/v1/movies/</span><span class="p">${</span><span class="nx">movieObject</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// there should be no errors</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="c1">// there should be a 200 status code</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="c1">// the response should be JSON</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"status": "success"}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'success'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"data": 1 movie object}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">should</span><span class="p">.</span><span class="nx">include</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span>
<span class="s1">'id'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'explicit'</span>
<span class="p">);</span>
<span class="c1">// ensure the movie was in fact deleted</span>
<span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">).</span><span class="nx">select</span><span class="p">(</span><span class="s1">'*'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">updatedMovies</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">updatedMovies</span><span class="p">.</span><span class="nx">length</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="nx">lengthBeforeDelete</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="s1">'should throw an error if the movie does not exist'</span><span class="p">,</span> <span class="p">(</span><span class="nx">done</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">chai</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">server</span><span class="p">)</span>
<span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="s1">'/api/v1/movies/9999999'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">end</span><span class="p">((</span><span class="nx">err</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// there should an error</span>
<span class="nx">should</span><span class="p">.</span><span class="nx">exist</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="c1">// there should be a 404 status code</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">404</span><span class="p">);</span>
<span class="c1">// the response should be JSON</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">type</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="s1">'application/json'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"status": "error"}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'error'</span><span class="p">);</span>
<span class="c1">// the JSON response body should have a</span>
<span class="c1">// key-value pair of {"message": "That movie does not exist."}</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">message</span><span class="p">.</span><span class="nx">should</span><span class="p">.</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'That movie does not exist.'</span><span class="p">);</span>
<span class="nx">done</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Route handler:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">BASE_URL</span><span class="p">}</span><span class="s2">/:id`</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">movie</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">queries</span><span class="p">.</span><span class="nx">deleteMovie</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">movie</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">200</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'success'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="nx">movie</span>
<span class="p">};</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">404</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'error'</span><span class="p">,</span>
<span class="na">message</span><span class="p">:</span> <span class="s1">'That movie does not exist.'</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">400</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s1">'error'</span><span class="p">,</span>
<span class="na">message</span><span class="p">:</span> <span class="nx">err</span><span class="p">.</span><span class="nx">message</span> <span class="o">||</span> <span class="s1">'Sorry, an error has occurred.'</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">deleteMovie</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">knex</span><span class="p">(</span><span class="s1">'movies'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">del</span><span class="p">()</span>
<span class="p">.</span><span class="nx">where</span><span class="p">({</span> <span class="na">id</span><span class="p">:</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span> <span class="p">})</span>
<span class="p">.</span><span class="nx">returning</span><span class="p">(</span><span class="s1">'*'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Run the tests to ensure all pass:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>routes : index
GET /
✓ should return json
routes : movies
GET /api/v1/movies
✓ should return all movies
GET /api/v1/movies/:id
✓ should respond with a single movie
✓ should throw an error if the movie does not exist
POST /api/v1/movies
✓ should return the movie that was added
✓ should throw an error if the payload is malformed
PUT /api/v1/movies
✓ should return the movie that was updated
✓ should throw an error if the movie does not exist
DELETE /api/v1/movies/:id
✓ should return the movie that was deleted
✓ should throw an error if the movie does not exist
Sample Test
✓ should pass
11 passing (697ms)
</code></pre></div></div>
<h2 id="next-steps">Next Steps</h2>
<p>With that, you now have a basic Koa RESTful API up and running.</p>
<p>Test your knowledge by adding additional test cases and error handlers to cover anything missed. You may also want to convert an existing Express app over to Koa. Check out the <a href="https://github.com/koajs/examples">Koa Examples</a> repo for more code examples.</p>
<p>Add end-to-end tests with <a href="http://mherman.org/blog/2017/03/19/functional-testing-with-testcafe/#.WZxCvnd95E4">TestCafe</a>.</p>
<p>This tutorial took advantage of async/await functions in Koa version 2. If you’re interested in comparing this pattern to the generator pattern found in Koa 1, review the code in the <a href="https://github.com/mjhea0/koa-api">Koa API</a> repo.</p>
<p>Finally, check out the following posts that build on the Koa app built in this post:</p>
<ol>
<li><a href="http://mherman.org/blog/2017/11/06/stubbing-http-requests-with-sinon">Stubbing HTTP Requests with Sinon</a></li>
<li><a href="http://mherman.org/blog/2018/01/02/user-authentication-with-passport-and-koa">User Authentication with Passport and Koa</a></li>
<li><a href="http://mherman.org/blog/2018/01/22/stubbing-node-authentication-middleware-with-sinon">Stubbing Node Authentication Middleware with Sinon</a></li>
</ol>
<p>Grab the final code from the <a href="https://github.com/mjhea0/node-koa-api/releases/tag/v2">v2</a> tag of <a href="https://github.com/mjhea0/node-koa-api">node-koa-api</a> repo. There’s <a href="http://mherman.org/presentations/node-koa-api">slides</a> as well.</p>
<p>Please add questions and/or comments below. Cheers!</p>
Michael Herman
In this tutorial, you’ll learn how to develop a RESTful API with Koa 2 and Postgres. You’ll also be taking advantage of async/await functions, from ES2017, and test driven development (TDD).
Developing Microservices - Node, React, and Docker
2017-05-11T00:00:00-05:00
2017-05-11T00:00:00-05:00
https://mherman.org/blog/developing-microservices-node-react-docker
<p>In this post you will learn how to quickly spin up a reproducible development environment with Docker to manage a number of Node.js microservices.</p>
<div style="text-align:center;">
<img src="/assets/img/blog/docker-microservices.png" style="max-width: 100%; border:0; box-shadow: none;" alt="microservice architecture" />
</div>
<p><br /></p>
<p>This post assumes prior knowledge of the following topics. Refer to the resources for more info:</p>
<table>
<thead>
<tr>
<th>Topic</th>
<th>Resource</th>
</tr>
</thead>
<tbody>
<tr>
<td>Docker</td>
<td><a href="https://docs.docker.com/engine/getstarted/">Get started with Docker</a></td>
</tr>
<tr>
<td>Docker Compose</td>
<td><a href="https://docs.docker.com/compose/gettingstarted/">Get started with Docker Compose</a></td>
</tr>
<tr>
<td>Node/Express API</td>
<td><a href="http://mherman.org/blog/2016/09/12/testing-node-and-express">Testing Node and Express</a></td>
</tr>
<tr>
<td>React</td>
<td><a href="https://github.com/mjhea0/node-workshop/blob/master/w2/lessons/03-react.md">React Intro</a></td>
</tr>
<tr>
<td>TestCafe</td>
<td><a href="http://mherman.org/blog/2017/03/19/functional-testing-with-testcafe">Functional Testing With TestCafe</a></td>
</tr>
<tr>
<td>Swagger</td>
<td><a href="http://mherman.org/blog/2016/05/26/swagger-and-nodejs/">Swagger and NodeJS</a></td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>NOTE</strong>: Looking for a slightly easier implementation? Check out my previous post - <a href="http://mherman.org/blog/2017/04/18/developing-and-testing-microservices-with-docker">Developing and Testing Microservices With Docker</a>.</p>
</blockquote>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#architecture" id="markdown-toc-architecture">Architecture</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#users-service" id="markdown-toc-users-service">Users Service</a></li>
<li><a href="#web-service---part-1" id="markdown-toc-web-service---part-1">Web Service - part 1</a></li>
<li><a href="#movies-service" id="markdown-toc-movies-service">Movies Service</a></li>
<li><a href="#web-service---part-2" id="markdown-toc-web-service---part-2">Web Service - part 2</a></li>
<li><a href="#workflow" id="markdown-toc-workflow">Workflow</a></li>
<li><a href="#test-setup" id="markdown-toc-test-setup">Test Setup</a></li>
<li><a href="#swagger-setup" id="markdown-toc-swagger-setup">Swagger Setup</a></li>
<li><a href="#next-steps" id="markdown-toc-next-steps">Next Steps</a></li>
</ul>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you should be able to…</p>
<ol>
<li>Configure and run microservices locally with Docker and Docker Compose</li>
<li>Utilize <a href="https://docs.docker.com/engine/tutorials/dockervolumes/">volumes</a> to mount your code into a container</li>
<li>Run unit and integration tests inside a Docker container</li>
<li>Test the entire set of services with functional, end-to-end tests</li>
<li>Debug a running Docker container</li>
<li>Enable services running in different containers to talk to one other</li>
<li>Secure your services via JWT-based authentication</li>
<li>Work with React running inside a Docker Container</li>
<li>Configure Swagger to interact with a service</li>
</ol>
<h2 id="architecture">Architecture</h2>
<p>The end goal of this post is to organize the technologies from the above image into the following containers and services:</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Service</th>
<th>Container</th>
<th>Tech</th>
</tr>
</thead>
<tbody>
<tr>
<td>Web</td>
<td>Web</td>
<td>web</td>
<td>React, React-Router</td>
</tr>
<tr>
<td>Movies API</td>
<td>Movies</td>
<td>movies</td>
<td>Node, Express</td>
</tr>
<tr>
<td>Movies DB</td>
<td>Movies</td>
<td>movies-db</td>
<td>Postgres</td>
</tr>
<tr>
<td>Swagger</td>
<td>Movies</td>
<td>swagger</td>
<td>Swagger UI</td>
</tr>
<tr>
<td>Users API</td>
<td>Users</td>
<td>users</td>
<td>Node, Express</td>
</tr>
<tr>
<td>Users DB</td>
<td>Users</td>
<td>users-db</td>
<td>Postgres</td>
</tr>
<tr>
<td>Functional Tests</td>
<td>Test</td>
<td>n/a</td>
<td>TestCafe</td>
</tr>
</tbody>
</table>
<p>Let’s get started!</p>
<h2 id="project-setup">Project Setup</h2>
<p>Start by cloning the base project and then checking out the first tag:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/mjhea0/microservice-movies
<span class="nv">$ </span><span class="nb">cd </span>microservice-movies
<span class="nv">$ </span>git checkout tags/v1
</code></pre></div></div>
<p>Overall project structure:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">.</span>
├── services
│ ├── movies
│ │ ├── src
│ │ │ └── db
│ │ └── swagger
│ ├── users
│ │ └── src
│ │ └── db
│ └── web
└── tests
</code></pre></div></div>
<p>Before we add Docker, be sure to review the code so that you have a basic understanding of how everything works. Feel free to test these services as well…</p>
<p><em>Users:</em></p>
<ul>
<li>Navigate to “services/users”</li>
<li><code class="highlighter-rouge">npm install</code></li>
<li>update the <code class="highlighter-rouge">start</code> script within <em>package.json</em> to <code class="highlighter-rouge">"gulp --gulpfile gulpfile.js"</code></li>
<li><code class="highlighter-rouge">npm start</code></li>
<li>Open <a href="http://localhost:3000/users/ping">http://localhost:3000/users/ping</a> in your browser</li>
</ul>
<p><em>Movies:</em></p>
<ul>
<li>Navigate to “services/movies”</li>
<li><code class="highlighter-rouge">npm install</code></li>
<li>update the <code class="highlighter-rouge">start</code> script within <em>package.json</em> to <code class="highlighter-rouge">"gulp --gulpfile gulpfile.js"</code></li>
<li><code class="highlighter-rouge">npm start</code></li>
<li>Open <a href="http://localhost:3000/movies/ping">http://localhost:3000/movies/ping</a> in your browser</li>
</ul>
<p><em>Web:</em></p>
<ul>
<li>Navigate to “services/web”</li>
<li><code class="highlighter-rouge">npm install</code></li>
<li><code class="highlighter-rouge">npm start</code></li>
<li>Open <a href="http://localhost:3006">http://localhost:3006</a> in your browser. You should see the log in page.</li>
</ul>
<p>Next, add a <em>docker-compose.yml</em> file to the project root. This file is used by Docker Compose to link multiple services together. With one command it will spin up all the containers we need and enable them to communicate with one another (as needed).</p>
<p>With that, let’s get each service going, making sure to test as we go…</p>
<h2 id="users-service">Users Service</h2>
<p>We’ll start with the database since the API is dependent on it being up…</p>
<h3 id="database">Database</h3>
<p>First, add a <em>Dockerfile</em> to “services/users/src/db”:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM postgres
# run create.sql on init
ADD create.sql /docker-entrypoint-initdb.d
</code></pre></div></div>
<p>Here, we extend the official Postgres image by adding a SQL file to the “docker-entrypoint-initdb.d” directory in the container, which will execute on init.</p>
<p>Then update the <em>docker-compose.yml</em> file:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>version: '2.1'
services:
users-db:
container_name: users-db
build: ./services/users/src/db
ports:
- '5433:5432' # expose ports - HOST:CONTAINER
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
healthcheck:
test: exit 0
</code></pre></div></div>
<p>This config will create a container called <code class="highlighter-rouge">users-db</code>, from the <em>Dockerfile</em> found in “services/users/src/db”. (Directories are relative to the <em>docker-compose.yml</em> file.)</p>
<p>Once spun up, environment variables will be added and an exit code of <code class="highlighter-rouge">0</code> will be sent after it’s successfully up and running. Postgres will be available on port <code class="highlighter-rouge">5433</code> on the host machine and on port <code class="highlighter-rouge">5432</code> for other services.</p>
<blockquote>
<p><strong>NOTE:</strong> Use <code class="highlighter-rouge">expose</code>, rather than <code class="highlighter-rouge">ports</code>, if you just want Postgres available to other services but not the host machine:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>expose:
- "5432"
</code></pre></div> </div>
</blockquote>
<p>Take note of the version used - <code class="highlighter-rouge">2.1</code>. This does not relate directly to the version of Docker Compose installed; instead, it specifies the file format that you want to use.</p>
<p>Fire up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">--build</span> <span class="nt">-d</span> users-db
</code></pre></div></div>
<p>Once up, let’s run a quick sanity check. Enter the shell:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose run users-db bash
</code></pre></div></div>
<p>Then run <code class="highlighter-rouge">env</code> to ensure that the proper environment variables are set. You can also check out the “docker-entrypoint-initdb.d” directory:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># cd docker-entrypoint-initdb.d/</span>
<span class="c"># ls</span>
create.sql
</code></pre></div></div>
<p><code class="highlighter-rouge">exit</code> when done.</p>
<h3 id="api">API</h3>
<p>Turning to the API, add a <em>Dockerfile</em> to “services/users”, making sure to review the comments:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM node:latest
# set working directory
RUN mkdir /usr/src/app
WORKDIR /usr/src
# add `/usr/src/node_modules/.bin` to $PATH
ENV PATH /usr/src/node_modules/.bin:$PATH
# install and cache app dependencies
ADD package.json /usr/src/package.json
RUN npm install
# start app
CMD ["npm", "start"]
</code></pre></div></div>
<blockquote>
<p><strong>NOTE</strong>: Be sure to take advantage of Docker’s layered <a href="https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#build-cache">cache</a> system, to speed up build times, by adding the <em>package.json</em> and installing the dependencies <strong>before</strong> adding the app’s source files. For more on this, check out <a href="http://bitjudo.com/blog/2014/03/13/building-efficient-dockerfiles-node-dot-js/">Building Efficient Dockerfiles - Node.js</a>.</p>
</blockquote>
<p>Then add the <code class="highlighter-rouge">users-service</code> to the <em>docker-compose.yml</em> file:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>users-service:
container_name: users-service
build: ./services/users/
volumes:
- './services/users:/usr/src/app'
- './services/users/package.json:/usr/src/package.json'
ports:
- '3000:3000' # expose ports - HOST:CONTAINER
environment:
- DATABASE_URL=postgres://postgres:postgres@users-db:5432/users_dev
- DATABASE_TEST_URL=postgres://postgres:postgres@users-db:5432/users_test
- NODE_ENV=${NODE_ENV}
- TOKEN_SECRET=changeme
depends_on:
users-db:
condition: service_healthy
links:
- users-db
</code></pre></div></div>
<p>What’s happening here?</p>
<ul>
<li><code class="highlighter-rouge">volumes</code>: <a href="https://docs.docker.com/engine/tutorials/dockervolumes/">volumes</a> are used to mount a directory inside a container so that you can make modifications to the code without having to rebuild the image. This should be a default in your local development environment so you quickly get feedback on code changes.</li>
<li><code class="highlighter-rouge">depends_on</code>: <a href="https://docs.docker.com/compose/compose-file/#dependson">depends_on</a> specifies the order in which to start services. In this case, the <code class="highlighter-rouge">users-service</code> will wait for the <code class="highlighter-rouge">users-db</code> to fire up successfully (with an exit code of <code class="highlighter-rouge">0</code>) before it starts.</li>
<li><code class="highlighter-rouge">links</code>: With <a href="https://docs.docker.com/compose/compose-file/#links">links</a> you can link to services running in other containers. So, with this config, code inside the <code class="highlighter-rouge">users-service</code> will be able to access the database via <code class="highlighter-rouge">users-db:5432</code>.</li>
</ul>
<blockquote>
<p><strong>NOTE:</strong> Curious about the difference between <code class="highlighter-rouge">depends_on</code> and <code class="highlighter-rouge">links</code>? Check out the following <a href="http://stackoverflow.com/a/39658359/1799408">Stack Overflow discussion</a> for more info.</p>
</blockquote>
<p>Set the <code class="highlighter-rouge">NODE_ENV</code> environment variable:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">export </span><span class="nv">NODE_ENV</span><span class="o">=</span>development
</code></pre></div></div>
<p>Build the image and spin up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">--build</span> <span class="nt">-d</span> users-service
</code></pre></div></div>
<blockquote>
<p><strong>NOTE:</strong> Keep in mind that Docker Compose handles both the build and run times. This can be confusing. For example, take a look at the current docker-compose.yml file - What is happening at the build time? How about the run time? How do you know?</p>
</blockquote>
<p>Once up, create a new file in the project root called <em>init_db.sh</em> and add the Knex migrate and seed commands:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>
docker-compose run users-service knex migrate:latest <span class="nt">--env</span> development <span class="nt">--knexfile</span> app/knexfile.js
docker-compose run users-service knex seed:run <span class="nt">--env</span> development <span class="nt">--knexfile</span> app/knexfile.js
</code></pre></div></div>
<p>Then apply the migrations and add the seed:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>sh init_db.sh
Using environment: development
Batch 1 run: 1 migrations
/src/src/db/migrations/20170504191016_users.js
Using environment: development
Ran 1 seed files
/src/src/db/seeds/users.js
</code></pre></div></div>
<p>Test:</p>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>HTTP Method</th>
<th>CRUD Method</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>/users/ping</td>
<td>GET</td>
<td>READ</td>
<td><code class="highlighter-rouge">pong</code></td>
</tr>
<tr>
<td>/users/register</td>
<td>POST</td>
<td>CREATE</td>
<td>add a user</td>
</tr>
<tr>
<td>/users/login</td>
<td>POST</td>
<td>CREATE</td>
<td>log in a user</td>
</tr>
<tr>
<td>/users/user</td>
<td>GET</td>
<td>READ</td>
<td>get user info</td>
</tr>
</tbody>
</table>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>http POST http://localhost:3000/users/register <span class="nv">username</span><span class="o">=</span>foo <span class="nv">password</span><span class="o">=</span>bar
<span class="nv">$ </span>http POST http://localhost:3000/users/login <span class="nv">username</span><span class="o">=</span>foo <span class="nv">password</span><span class="o">=</span>bar
</code></pre></div></div>
<blockquote>
<p><strong>NOTE:</strong> <code class="highlighter-rouge">http</code> in the above commands is part of the <a href="https://httpie.org/">HTTPie</a> library, which is a wrapper on top of cURL.</p>
</blockquote>
<p>In both cases you should see a <code class="highlighter-rouge">status</code> of <code class="highlighter-rouge">success</code> along with a <code class="highlighter-rouge">token</code>, i.e. -</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">{</span>
<span class="s2">"status"</span>: <span class="s2">"success"</span>,
<span class="s2">"token"</span>: <span class="s2">"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Finally, run the unit and integration tests:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose run users-service npm <span class="nb">test</span>
</code></pre></div></div>
<p>You should see:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>routes : index
GET /does/not/exist
✓ should throw an error
routes : users
POST /users/register
✓ should register a new user (178ms)
POST /users/login
✓ should login a user (116ms)
✓ should not login an unregistered user
✓ should not login a valid user with incorrect password (125ms)
GET /users/user
✓ should return a success (114ms)
✓ should throw an error if a user is not logged in
auth : helpers
comparePass()
✓ should return true if the password is correct (354ms)
✓ should return false if the password is correct (315ms)
✓ should return false if the password empty (305ms)
auth : local
encodeToken()
✓ should return a token
decodeToken()
✓ should return a payload
12 passing (4s)
</code></pre></div></div>
<p>Check the test specs for more info. That’s it! Let’s move on to the web service…</p>
<h2 id="web-service---part-1">Web Service - part 1</h2>
<p>With our users service up and running, we can turn our attention to the client-side and spin up the React app inside a container to test authentication.</p>
<blockquote>
<p><strong>NOTE:</strong> The React code is ported from <a href="https://github.com/blackstc/intro-react-redux-omdb">intro-react-redux-omdb</a> and <a href="https://github.com/etmoore/communikey">communikey</a> written by <a href="https://www.linkedin.com/in/charlieblackstock/">Charlie Blackstock</a> and <a href="https://www.linkedin.com/in/etmoore1/">Evan Moore</a>, respectively - two of my former students.</p>
</blockquote>
<p>Add a <em>Dockerfile</em> to “services/web”:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM node:latest
# set working directory
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
# add `/usr/src/app/node_modules/.bin` to $PATH
ENV PATH /usr/src/app/node_modules/.bin:$PATH
# install and cache app dependencies
ADD package.json /usr/src/app/package.json
RUN npm install
RUN npm install react-scripts@0.9.5 -g
# start app
CMD ["npm", "start"]
</code></pre></div></div>
<p>As of 05/10/2017 the <a href="http://www.omdbapi.com/">OMDb API</a> is private, so you have to donate at least $1 to gain access. Once you have an API Key, update the <code class="highlighter-rouge">API_URL</code> in <em>services/web/src/App.jsx</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">API_URL</span> <span class="o">=</span> <span class="s1">'http://www.omdbapi.com/?apikey=addyourkey&s='</span>
</code></pre></div></div>
<p>Then update the <em>docker-compose.yml</em> file like so:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>web-service:
container_name: web-service
build: ./services/web/
volumes:
- './services/web:/usr/src/app'
- '/usr/src/app/node_modules'
ports:
- '3007:3006' # expose ports - HOST:CONTAINER
environment:
- NODE_ENV=${NODE_ENV}
depends_on:
users-service:
condition: service_started
links:
- users-service
</code></pre></div></div>
<blockquote>
<p><strong>NOTE:</strong> To prevent the volume - <code class="highlighter-rouge">/usr/src/app</code> - from overriding the <em>package.json</em>, we used a data volume - <code class="highlighter-rouge">/usr/src/app/node_modules</code>. This may or may not be necessary, depending on the order in which you set up your image and containers. Check out <a href="http://dchua.com/2016/02/07/getting-npm-packages-to-be-installed-with-docker-compose/">Getting npm packages to be installed with docker-compose</a> for more.</p>
</blockquote>
<p>Build the image and fire up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">--build</span> <span class="nt">-d</span> web-service
</code></pre></div></div>
<blockquote>
<p><strong>NOTE:</strong> To avoid dealing with too much configuration (babel and webpack), the React app uses <a href="https://github.com/facebookincubator/create-react-app">Create React App</a>.</p>
</blockquote>
<p>Open your browser and navigate to <a href="http://localhost:3007">http://localhost:3007</a>. You should see the login page:</p>
<div style="text-align:center;">
<img src="/assets/img/blog/microservice-movies-login.png" style="max-width: 100%; border:0; box-shadow: none;" alt="login page" />
</div>
<p>Log in with -</p>
<ul>
<li>username: <code class="highlighter-rouge">foo</code></li>
<li>password: <code class="highlighter-rouge">bar</code></li>
</ul>
<p>Once logged in you should see:</p>
<div style="text-align:center;">
<img src="/assets/img/blog/microservice-movies-search.png" style="max-width: 100%; border:0; box-shadow: none;" alt="search page" />
</div>
<p>Within <em>services/web/src/App.jsx</em>, let’s take a quick look at the AJAX request in the <code class="highlighter-rouge">loginUser()</code> method:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">loginUser</span> <span class="p">(</span><span class="nx">userData</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*
why? http://localhost:3000/users/login
why not? http://users-service:3000/users/login
*/</span>
<span class="k">return</span> <span class="nx">axios</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">'http://localhost:3000/users/login'</span><span class="p">,</span> <span class="nx">userData</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">'authToken'</span><span class="p">,</span> <span class="nx">res</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">token</span><span class="p">)</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">'user'</span><span class="p">,</span> <span class="nx">res</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">user</span><span class="p">)</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span> <span class="na">isAuthenticated</span><span class="p">:</span> <span class="kc">true</span> <span class="p">})</span>
<span class="k">this</span><span class="p">.</span><span class="nx">createFlashMessage</span><span class="p">(</span><span class="s1">'You successfully logged in! Welcome!'</span><span class="p">)</span>
<span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">history</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s1">'/'</span><span class="p">)</span>
<span class="k">this</span><span class="p">.</span><span class="nx">getMovies</span><span class="p">()</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">callback</span><span class="p">(</span><span class="s1">'Something went wrong'</span><span class="p">)</span>
<span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Why do we use <code class="highlighter-rouge">localhost</code> rather than the name of the container, <code class="highlighter-rouge">users-service</code>? This request is originating outside the container, on the host. Keep in mind, that if, this request was originating inside the container, we would need to use the container name rather than <code class="highlighter-rouge">localhost</code>, since <code class="highlighter-rouge">localhost</code> would refer back to the container itself in that situation.</p>
<p>Make sure you can log out and register as well.</p>
<p>Next, let’s spin up the movies service so that end users can save movies to a collection…</p>
<h2 id="movies-service">Movies Service</h2>
<p>Set up for the movies service is nearly the same as the users service. Try this on your own to check your understanding:</p>
<ol>
<li>Database
<ul>
<li>add a <em>Dockerfile</em></li>
<li>update the <em>docker-compose.yml</em></li>
<li>spin up the container</li>
<li>test</li>
</ul>
</li>
<li>API
<ul>
<li>add a <em>Dockerfile</em></li>
<li>update the <em>docker-compose.yml</em> (make sure to link the service with the database and the users service and update the exposed ports - <code class="highlighter-rouge">3001</code> for the api, <code class="highlighter-rouge">5434</code> for the db)</li>
<li>spin up the container</li>
<li>apply migrations and seeds</li>
<li>test</li>
</ul>
</li>
</ol>
<blockquote>
<p><strong>NOTE:</strong> Need help? Grab the code from the <a href="https://github.com/mjhea0/microservice-movies/releases/tag/v2">v2</a> tag of the <a href="https://github.com/mjhea0/microservice-movies">microservice-movies</a> repo.</p>
</blockquote>
<p>The movies database image should take much less time to build than the users database. Why?</p>
<p>With the containers up, let’s test out the endpoints…</p>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>HTTP Method</th>
<th>CRUD Method</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>/movies/ping</td>
<td>GET</td>
<td>READ</td>
<td><code class="highlighter-rouge">pong</code></td>
</tr>
<tr>
<td>/movies/user</td>
<td>GET</td>
<td>READ</td>
<td>get all movies by user</td>
</tr>
<tr>
<td>/movies</td>
<td>POST</td>
<td>CREATE</td>
<td>add a single movie</td>
</tr>
</tbody>
</table>
<p>Start with opening the browser to <a href="http://localhost:3001/movies/ping">http://localhost:3001/movies/ping</a>. You should see <code class="highlighter-rouge">pong</code>! Try <a href="http://localhost:3001/movies/user">http://localhost:3001/movies/user</a>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Please log in"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Since you need to be authenticated to access the other routes, let’s test them out by running the integration tests:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose run movies-service npm <span class="nb">test</span>
</code></pre></div></div>
<p>You should see:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>routes : index
GET /does/not/exist
✓ should throw an error
Movies API Routes
GET /movies/ping
✓ should <span class="k">return</span> <span class="s2">"pong"</span>
GET /movies/user
✓ should <span class="k">return </span>saved movies
POST /movies
✓ should create a new movie
4 passing <span class="o">(</span>818ms<span class="o">)</span>
</code></pre></div></div>
<p>Check the test specs for more info.</p>
<h2 id="web-service---part-2">Web Service - part 2</h2>
<p>Turn to the <em>docker-compose.yml</em> file. Update the <code class="highlighter-rouge">links</code> and <code class="highlighter-rouge">depends_on</code> keys for the <code class="highlighter-rouge">web-service</code>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>depends_on:
users-service:
condition: service_started
movies-service:
condition: service_started
links:
- users-service
- movies-service
</code></pre></div></div>
<p>Why?</p>
<p>Next, update the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">-d</span> web-service
</code></pre></div></div>
<p>Let’s test this out in the browser! Open <a href="http://localhost:3007/">http://localhost:3007/</a>. Register a new user and then add some movies to the collection.</p>
<p>Be sure to view the collection as well:</p>
<div style="text-align:center;">
<img src="/assets/img/blog/microservice-movies-collection.png" style="max-width: 100%; border:0; box-shadow: none;" alt="collection page" />
</div>
<p>Open <em>services/movies/src/routes/_helpers.js</em> and take note of the <code class="highlighter-rouge">ensureAuthenticated()</code> method:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">ensureAuthenticated</span> <span class="o">=</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">next</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">headers</span> <span class="o">&&</span> <span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">authorization</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">400</span><span class="p">).</span><span class="nx">json</span><span class="p">({</span> <span class="na">status</span><span class="p">:</span> <span class="s1">'Please log in'</span> <span class="p">});</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="s1">'GET'</span><span class="p">,</span>
<span class="na">uri</span><span class="p">:</span> <span class="s1">'http://users-service:3000/users/user'</span><span class="p">,</span>
<span class="na">json</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'Content-Type'</span><span class="p">:</span> <span class="s1">'application/json'</span><span class="p">,</span>
<span class="na">Authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">authorization</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s1">' '</span><span class="p">)[</span><span class="mi">1</span><span class="p">]}</span><span class="s2">`</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">};</span>
<span class="k">return</span> <span class="nx">request</span><span class="p">(</span><span class="nx">options</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">req</span><span class="p">.</span><span class="nx">user</span> <span class="o">=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">user</span><span class="p">;</span>
<span class="k">return</span> <span class="nx">next</span><span class="p">();</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="k">return</span> <span class="nx">next</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span> <span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Why does the uri point to <code class="highlighter-rouge">users-service</code> and not <code class="highlighter-rouge">localhost</code>?</p>
<h2 id="workflow">Workflow</h2>
<p>Start by checking out the <em>Workflow</em> section from <a href="http://mherman.org/blog/2017/04/18/developing-and-testing-microservices-with-docker/">Developing and Testing Microservices With Docker</a>. Experiment with live reloading on a code change and debugging a running container with <code class="highlighter-rouge">console.log</code>.</p>
<p>Add a header to the collection page:</p>
<div style="text-align:center;">
<img src="/assets/img/blog/microservice-movies-collection-updated.png" style="max-width: 100%; border:0; box-shadow: none;" alt="collection page with header" />
</div>
<p>Run the logs - <code class="highlighter-rouge">docker-compose logs -f web-service</code> - and then make a change to one of the components that breaks compilation:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>web-service | Compiling...
web-service | Failed to compile.
web-service |
web-service | Error <span class="k">in</span> ./src/components/SavedMovies.jsx
web-service |
web-service | /usr/src/app/src/components/SavedMovies.jsx
web-service | 10:13 error <span class="s1">'Link'</span> is not defined react/jsx-no-undef
web-service |
web-service | ✖ 1 problem <span class="o">(</span>1 error, 0 warnings<span class="o">)</span>
web-service |
web-service |
</code></pre></div></div>
<p>Correct the error:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>web-service |
web-service | Compiling...
web-service | Compiled successfully!
</code></pre></div></div>
<p>Continue to experiment with adding and updating the React app until you feel comfortable working with it inside the container.</p>
<h2 id="test-setup">Test Setup</h2>
<p>Thus far we’ve only tested each individual microservice with unit and integration tests. Let’s turn our attention to functional, end-to-end tests to test the entire system. For this, we’ll use <a href="https://devexpress.github.io/testcafe/">TestCafe</a>.</p>
<blockquote>
<p><strong>NOTE:</strong> Don’t want to use TestCafe? Check out the <a href="https://github.com/mjhea0/node-docker-api/tree/master/tests">code</a> for using Mocha, Chai, Request, and Cheerio (all within a container) for testing.</p>
</blockquote>
<p>Let’s be lazy and install TestCafe globally:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install testcafe@0.15.0 <span class="nt">-g</span>
</code></pre></div></div>
<p>Then run the tests:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>testcafe firefox tests/<span class="k">**</span>/<span class="k">*</span>.js
</code></pre></div></div>
<p>You should see:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>testcafe firefox tests/<span class="k">**</span>/<span class="k">*</span>.js
Running tests <span class="k">in</span>:
- Firefox 53.0.0 / Mac OS X 10.11.0
/login
✓ users should be able to log <span class="k">in </span>and out
1 passed <span class="o">(</span>3s<span class="o">)</span>
</code></pre></div></div>
<blockquote>
<p><strong>NOTE:</strong> Interested in running the tests from within a container? Check out the <a href="https://devexpress.github.io/testcafe/documentation/using-testcafe/installing-testcafe.html#using-testcafe-docker-image">official TestCafe docs</a> for more info on using TestCafe with Docker.</p>
</blockquote>
<p>To simplify the test workflow, add a <em>test.sh</em> file to the project root:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nv">fails</span><span class="o">=</span><span class="s1">''</span>
inspect<span class="o">()</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">[</span> <span class="nv">$1</span> <span class="nt">-ne</span> 0 <span class="o">]</span> <span class="p">;</span> <span class="k">then
</span><span class="nv">fails</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">fails</span><span class="k">}</span><span class="s2"> </span><span class="nv">$2</span><span class="s2">"</span>
<span class="k">fi</span>
<span class="o">}</span>
docker-compose run users-service npm <span class="nb">test
</span>inspect <span class="nv">$?</span> users-service
docker-compose run movies-service npm <span class="nb">test
</span>inspect <span class="nv">$?</span> movies-service
testcafe firefox tests/<span class="k">**</span>/<span class="k">*</span>.js
inspect <span class="nv">$?</span> e2e
<span class="k">if</span> <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="k">${</span><span class="nv">fails</span><span class="k">}</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span>
<span class="k">then
</span><span class="nb">echo</span> <span class="s2">"Tests failed: </span><span class="k">${</span><span class="nv">fails</span><span class="k">}</span><span class="s2">"</span>
<span class="nb">exit </span>1
<span class="k">else
</span><span class="nb">echo</span> <span class="s2">"Tests passed!"</span>
<span class="nb">exit </span>0
<span class="k">fi</span>
</code></pre></div></div>
<p>Run the tests:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>sh test.sh
</code></pre></div></div>
<h2 id="swagger-setup">Swagger Setup</h2>
<p>Add a <em>Dockerfile</em> to “services/movies/swagger”:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM node:latest
# set working directory
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
# add `/usr/src/node_modules/.bin` to $PATH
ENV PATH /usr/src/app/node_modules/.bin:$PATH
# install and cache app dependencies
ADD package.json /usr/src/app/package.json
RUN npm install
# start app
CMD ["npm", "start"]
</code></pre></div></div>
<p>Update <em>docker-compose.yml</em>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>swagger:
container_name: swagger
build: ./services/movies/swagger/
volumes:
- './services/movies/swagger:/usr/src/app'
- '/usr/src/app/node_modules'
ports:
- '3003:3001' # expose ports - HOST:CONTAINER
environment:
- NODE_ENV=${NODE_ENV}
depends_on:
users-service:
condition: service_started
movies-service:
condition: service_started
links:
- users-service
- movies-service
</code></pre></div></div>
<p>Fire it up:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">-d</span> <span class="nt">--build</span> swagger
</code></pre></div></div>
<p>Navigate to <a href="http://localhost:3003/docs">http://localhost:3003/docs</a> and test it out:</p>
<div style="text-align:center;">
<img src="/assets/img/blog/microservice-movies-swagger.png" style="max-width: 100%; border:0; box-shadow: none;" alt="swagger docs" />
</div>
<p>Now you just need to incorporate support for JWT-based auth and add the remaining endpoints!</p>
<h2 id="next-steps">Next Steps</h2>
<p>What’s next?</p>
<ol>
<li><em>React App</em> - The React app could use some love. Add styles. Fix bugs. Update the flash messages so that only one is displayed at a time. Write tests. Build new features. Add Redux. The sky’s the limit. Contact me if you’d like to pair!</li>
<li><em>Swagger</em> - Add JWT-based auth and add additional endpoints from the movies service.</li>
<li><em>Dockerfiles</em> - Read <a href="https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/">Best practices for writing Dockerfiles</a>, by the Docker team, and refactor as necessary.</li>
<li><em>Production</em> - Want to deploy on AWS? Check out the <a href="http://mherman.org/blog/2017/09/18/on-demand-test-environments-with-docker-and-aws-ecs">On-Demand Environments With Docker and AWS ECS</a> blog post.</li>
</ol>
<p>Grab the final code from the <a href="https://github.com/mjhea0/microservice-movies/releases/tag/v2">v2</a> tag of the <a href="https://github.com/mjhea0/microservice-movies">microservice-movies</a> repo. Please add questions and/or comments below. There’s slides too! Check them out <a href="http://mherman.org/microservice-movies">here</a>, if interested.</p>
Michael Herman
In this post you will learn how to quickly spin up a reproducible development environment with Docker to manage a number of Node.js microservices.
Flask for Node Developers
2017-04-26T00:00:00-05:00
2017-04-26T00:00:00-05:00
https://mherman.org/blog/flask-for-node-developers
<p>Today we’ll be going through how to build a basic CRUD server-side application using Python and <a href="http://flask.pocoo.org/">Flask</a>, geared toward JavaScript developers versed in Node and Express. Similar to <a href="https://expressjs.com/">Express</a>, <a href="http://flask.pocoo.org/">Flask</a> is a simple, yet powerful micro-framework for Python, perfect for RESTful APIs.</p>
<div style="text-align:center;">
<img src="/assets/img/blog/flask-node.png" style="max-width: 100%; border:0; box-shadow: none;" alt="flask and node" />
</div>
<p><br /></p>
<blockquote>
<p>This tutorial uses Flask v<a href="https://pypi.python.org/pypi/Flask/0.12.1">0.12.1</a> and Python v<a href="https://www.python.org/downloads/release/python-361/">3.6.1</a>.</p>
</blockquote>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#database-setup" id="markdown-toc-database-setup">Database Setup</a></li>
<li><a href="#routes" id="markdown-toc-routes">Routes</a></li>
<li><a href="#next-steps" id="markdown-toc-next-steps">Next Steps</a></li>
</ul>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you should be able to…</p>
<ol>
<li>Set up a Python development environment</li>
<li>Create and activate a virtual environment</li>
<li>Using SQLite, apply a schema to the database and interact with the database using the basic CRUD functions</li>
<li>Build a CRUD app using Python and Flask</li>
</ol>
<h2 id="project-setup">Project Setup</h2>
<p>Before we start, ensure that you have <a href="https://www.python.org/downloads/release/python-361/">Python v3.6.1</a> installed.</p>
<p>Along with Python, we also need <a href="https://pypi.python.org/pypi/pip">pip</a> to install third-party packages from the <a href="https://pypi.org/">Python Package Index</a> (aka PyPI), the Python equivalent of npm. Fortunately, this comes pre-installed with all Python versions >= 3.4.</p>
<p>Let’s start by creating a new project directory:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>mkdir flask-songs-api <span class="o">&&</span> <span class="nb">cd </span>flask-songs-api
</code></pre></div></div>
<p>Next up, we’ll create an isolated virtual environment for installing Python packages specific to our individual project. It’s <a href="https://www.python.org/dev/peps/pep-0405/#isolation-from-system-site-packages">standard practice</a> to set up a virtual environment for each project, otherwise there can be compatibility issues with different dependencies.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>python3.6 <span class="nt">-m</span> venv env
</code></pre></div></div>
<p>Next, we need to activate it:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">source </span>env/bin/activate
</code></pre></div></div>
<p>You should now see <code class="highlighter-rouge">env</code> in your prompt, indicating that the virtual environment is activated.</p>
<blockquote>
<p><strong>NOTE:</strong> Ready to stop developing? Use the <code class="highlighter-rouge">deactivate</code> command to deactivate the virtual environment. To activate it again, navigate to the directory and re-run the source command - <code class="highlighter-rouge">source env/bin/activate</code>.</p>
</blockquote>
<p>Now we can install Flask:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pip install <span class="nv">Flask</span><span class="o">==</span>0.12.1
<span class="nv">$ </span>pip freeze <span class="o">></span> requirements.txt
</code></pre></div></div>
<p>Now is a great time to add a <em>.gitignore</em>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>env
*.db
</code></pre></div></div>
<p>Finally, let’s add a main app file, which will handle routing and run our application, along with a file to setup our database schema:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>touch app.py models.py
</code></pre></div></div>
<h2 id="database-setup">Database Setup</h2>
<p>For this tutorial, we will be using <a href="https://www.sqlite.org/">SQLite3</a> since it’s part of the <a href="https://docs.python.org/3.5/library/sqlite3.html">Python standard library</a>, requires little (if any) configuration, and is powerful enough for small to mid-size applications (e.g., the majority of web apps).</p>
<p>Start by creating a new database file in your project root:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>touch songs.db
</code></pre></div></div>
<p>Now start a new SQLite session:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>sqlite3 songs.db
</code></pre></div></div>
<p>Then run:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlite> .databases
</code></pre></div></div>
<p>You should see a file path to your database file, which is empty at the moment and ready for us to create a schema and data to. To create a schema, add the following code to <em>models.py</em>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">sqlite3</span>
<span class="k">def</span> <span class="nf">drop_table</span><span class="p">():</span>
<span class="k">with</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s">'songs.db'</span><span class="p">)</span> <span class="k">as</span> <span class="n">connection</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">connection</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s">"""DROP TABLE IF EXISTS songs;"""</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">True</span>
<span class="k">def</span> <span class="nf">create_db</span><span class="p">():</span>
<span class="k">with</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s">'songs.db'</span><span class="p">)</span> <span class="k">as</span> <span class="n">connection</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">connection</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">table</span> <span class="o">=</span> <span class="s">"""CREATE TABLE songs(
id INTEGER PRIMARY KEY AUTOINCREMENT,
artist TEXT NOT NULL,
title TEXT NOT NULL,
rating INTEGER NOT NULL
);
"""</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">table</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">True</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
<span class="n">drop_table</span><span class="p">()</span>
<span class="n">create_db</span><span class="p">()</span>
</code></pre></div></div>
<p>This will drop the songs table if it exists and put a new one in place with the schema we’ve defined here. If you have any issues with your database later on, or if you just want to start fresh, you can always run this script to recreate the database. Back in the terminal, exit SQLite and then run the script to create our table:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlite> .exit
<span class="nv">$ </span>python models.py
</code></pre></div></div>
<p>Let’s check if that actually worked:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>sqlite3 songs.db
sqlite> .table
songs
sqlite> .schema
CREATE TABLE songs<span class="o">(</span>
id INTEGER PRIMARY KEY AUTOINCREMENT,
artist TEXT NOT NULL,
title TEXT NOT NULL,
rating INTEGER NOT NULL
<span class="o">)</span><span class="p">;</span>
sqlite> .exit
</code></pre></div></div>
<p>Great! So, now that our database is set up correctly, we can move on to setting up our app’s route handlers. For this post, we won’t be going into database migrations but if you ever want to change the schema, you can use <a href="https://flask-migrate.readthedocs.io/en/latest/">Flask-Migrate</a>.</p>
<h2 id="routes">Routes</h2>
<p>Let’s start with an overview of the routes, following RESTful principles:</p>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>Result</th>
<th>CRUD</th>
<th>HTTP</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="highlighter-rouge">/api/songs</code></td>
<td>Returns all songs</td>
<td>Read</td>
<td>GET</td>
</tr>
<tr>
<td><code class="highlighter-rouge">/api/song/<song_id></code></td>
<td>Returns a single song</td>
<td>Read</td>
<td>GET</td>
</tr>
<tr>
<td><code class="highlighter-rouge">/api/songs</code></td>
<td>Adds a single song</td>
<td>Create</td>
<td>POST</td>
</tr>
<tr>
<td><code class="highlighter-rouge">/api/song/<song_id></code></td>
<td>Updates a single song</td>
<td>Update</td>
<td>PUT</td>
</tr>
<tr>
<td><code class="highlighter-rouge">/api/song/<song_id></code></td>
<td>Deletes a single song</td>
<td>Delete</td>
<td>DELETE</td>
</tr>
</tbody>
</table>
<p>But first, before creating any routes, add the following code to <em>app.py</em>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">sqlite3</span>
<span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
<span class="n">app</span><span class="o">.</span><span class="n">debug</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">app</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>
</code></pre></div></div>
<p>Here we imported <code class="highlighter-rouge">sqlite3</code> along with the main <a href="http://flask.pocoo.org/docs/0.12/api/#application-object">Flask application object</a>, <code class="highlighter-rouge">Flask</code>, which creates an instance of Flask in our application. The Flask application object acts as the central object, which we can use as a way of calling our view functions, adding our URL rules, template configuration and much more. With that instance we can run the application using the <code class="highlighter-rouge">run</code> <a href="http://flask.pocoo.org/docs/0.12/api/#flask.Flask.run">method</a>. We also set the <a href="http://flask.pocoo.org/docs/0.12/api/#flask.Flask.debug">debug flag</a> to <code class="highlighter-rouge">True</code> so that the server live reloads when code changes and provides an interactive debugger when an exception is thrown.</p>
<blockquote>
<p><strong>NOTE:</strong> <code class="highlighter-rouge">if __name__ == '__main__'</code> states that this source file is our main program. Any files imported from other modules will have their name set to their module name. This is because you may sometimes have modules that could be executed directly as well as be imported into a main app file. This line means that the code in those modules will only execute when you want to run the module as a program, and not have it execute when someone just wants to import a module and execute it themselves.</p>
</blockquote>
<p>Finally, it’s important to note that any imports must go at the top of our <em>app.py</em> file. These must come before anything else in order to be used later on in our file.</p>
<p>Now, add the routes:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">sqlite3</span>
<span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">request</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>
<span class="nd">@app.route</span><span class="p">(</span><span class="s">'/api/songs'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s">'GET'</span><span class="p">,</span> <span class="s">'POST'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">collection</span><span class="p">():</span>
<span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'GET'</span><span class="p">:</span>
<span class="k">pass</span> <span class="c"># Handle GET all Request</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'POST'</span><span class="p">:</span>
<span class="k">pass</span> <span class="c"># Handle POST request</span>
<span class="nd">@app.route</span><span class="p">(</span><span class="s">'/api/song/<song_id>'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s">'GET'</span><span class="p">,</span> <span class="s">'PUT'</span><span class="p">,</span> <span class="s">'DELETE'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">resource</span><span class="p">(</span><span class="n">song_id</span><span class="p">):</span>
<span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'GET'</span><span class="p">:</span>
<span class="k">pass</span> <span class="c"># Handle GET single request</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'PUT'</span><span class="p">:</span>
<span class="k">pass</span> <span class="c"># Handle UPDATE request</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'DELETE'</span><span class="p">:</span>
<span class="k">pass</span> <span class="c"># Handle DELETE request</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
<span class="n">app</span><span class="o">.</span><span class="n">debug</span> <span class="o">=</span> <span class="bp">True</span>
<span class="n">app</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>
</code></pre></div></div>
<p>We imported <code class="highlighter-rouge">request</code>, which handles, well, HTTP requests (no surprises there). Let’s look at each method, staring with a POST:</p>
<h3 id="post">POST</h3>
<p>The first thing we need to do is add data to our database. Once we’ve done this, we can start building and testing the rest of our database/CRUD functions.</p>
<p>The process is simple:</p>
<ol>
<li>Create a connection to our database</li>
<li>Execute our SQL query to add a song</li>
<li>Commit the changes to the database</li>
<li>Close the database connection</li>
<li>Return an object</li>
</ol>
<p>We can write a single function to handle this. Let’s place all helper functions underneath the routes, to keep things nicely separated:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># helper functions</span>
<span class="k">def</span> <span class="nf">add_song</span><span class="p">(</span><span class="n">artist</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">rating</span><span class="p">):</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">with</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s">'songs.db'</span><span class="p">)</span> <span class="k">as</span> <span class="n">connection</span><span class="p">:</span>
<span class="n">cursor</span> <span class="o">=</span> <span class="n">connection</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">cursor</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s">"""
INSERT INTO songs (artist, title, rating) values (?, ?, ?);
"""</span><span class="p">,</span> <span class="p">(</span><span class="n">artist</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">rating</span><span class="p">,))</span>
<span class="n">result</span> <span class="o">=</span> <span class="p">{</span><span class="s">'status'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">'message'</span><span class="p">:</span> <span class="s">'Song Added'</span><span class="p">}</span>
<span class="k">except</span><span class="p">:</span>
<span class="n">result</span> <span class="o">=</span> <span class="p">{</span><span class="s">'status'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s">'message'</span><span class="p">:</span> <span class="s">'error'</span><span class="p">}</span>
<span class="k">return</span> <span class="n">result</span>
</code></pre></div></div>
<p>Now we can just use this function in our route handler, passing the correct arguments from an incoming POST request:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@app.route</span><span class="p">(</span><span class="s">'/api/songs'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s">'GET'</span><span class="p">,</span> <span class="s">'POST'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">collection</span><span class="p">():</span>
<span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'GET'</span><span class="p">:</span>
<span class="k">pass</span> <span class="c"># Handle GET all Request</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'POST'</span><span class="p">:</span>
<span class="n">data</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">form</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">add_song</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s">'artist'</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="s">'title'</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="s">'rating'</span><span class="p">])</span>
<span class="k">return</span> <span class="n">jsonify</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
</code></pre></div></div>
<p>So, we grabbed the values from the incoming form request, then called the <code class="highlighter-rouge">add_song()</code> function to add that song to the database, and, finally, returned the appropriate JSON response.</p>
<p>Make sure to add <code class="highlighter-rouge">jsonify</code> to the imports in order to send a JSON response back:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">jsonify</span>
</code></pre></div></div>
<p>Ready to test? Start the server in one terminal window:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>python app.py
</code></pre></div></div>
<p>Now, in another window use CURL to send a POST request:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">--data</span> <span class="s2">"artist='Hudson Mohawke'&title='Cbat'&rating=5"</span> http://localhost:5000/api/songs
</code></pre></div></div>
<p>If all went well, you should see the following response, indicating that the song was added to the database:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">{</span>
<span class="s2">"message"</span>: <span class="s2">"Song Added"</span>,
<span class="s2">"status"</span>: 1
<span class="o">}</span>
</code></pre></div></div>
<p>Just to be on the safe side, let’s double-check that. Kill the server, then open a SQLite session from within your project directory:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>sqlite3 songs.db
</code></pre></div></div>
<p>Now run the following SQL query:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlite> SELECT <span class="k">*</span> FROM songs ORDER BY id desc<span class="p">;</span>
</code></pre></div></div>
<p>You should see:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1|'Hudson Mohawke'|'Cbat'|5`
</code></pre></div></div>
<p>Okay. We have officially added our first song! Add a couple more before moving on to reading data (GET). Don’t forget to run the Flask server before running the CURL commands!</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">--data</span> <span class="s2">"artist='Beastie Boys'&title='Sabotage'&rating=4"</span> http://localhost:5000/api/songs
<span class="nv">$ </span>curl <span class="nt">--data</span> <span class="s2">"artist='Gregori Klosman'&title='Jaws'&rating=3"</span> http://localhost:5000/api/songs
</code></pre></div></div>
<h3 id="get">GET</h3>
<p>We’ll start with our GET all route, e.g. - <code class="highlighter-rouge">api/songs</code>. We need to connect to the database, execute the appropriate SQL query, and then return all of the songs from that query:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_all_songs</span><span class="p">():</span>
<span class="k">with</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s">'songs.db'</span><span class="p">)</span> <span class="k">as</span> <span class="n">connection</span><span class="p">:</span>
<span class="n">cursor</span> <span class="o">=</span> <span class="n">connection</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">cursor</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s">"SELECT * FROM songs ORDER BY id desc"</span><span class="p">)</span>
<span class="n">all_songs</span> <span class="o">=</span> <span class="n">cursor</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
<span class="k">return</span> <span class="n">all_songs</span>
</code></pre></div></div>
<p>Next up, we have to change our route handler to now call this function and then send back JSON:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@app.route</span><span class="p">(</span><span class="s">'/api/songs'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s">'GET'</span><span class="p">,</span> <span class="s">'POST'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">collection</span><span class="p">():</span>
<span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'GET'</span><span class="p">:</span>
<span class="n">all_songs</span> <span class="o">=</span> <span class="n">get_all_songs</span><span class="p">()</span>
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">all_songs</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'POST'</span><span class="p">:</span>
<span class="n">data</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">form</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">add_song</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s">'artist'</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="s">'title'</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="s">'rating'</span><span class="p">])</span>
<span class="k">return</span> <span class="n">jsonify</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
</code></pre></div></div>
<p>Did you notice that we’re using the <code class="highlighter-rouge">json</code> module? This is also from the Python standard library, which allows us to convert the ‘list’-like format of data we get back from SQLite3 into a JSON object. Just don’t forget to import it:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">json</span>
</code></pre></div></div>
<p>To test, we can simply navigate to <a href="http://127.0.0.1:5000/api/songs">http://127.0.0.1:5000/api/songs</a> in the browser to check if all our songs are there.</p>
<p>You should see something like:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
</span><span class="p">[</span><span class="w">
</span><span class="mi">3</span><span class="p">,</span><span class="w">
</span><span class="s2">"'Gregori Klosman'"</span><span class="p">,</span><span class="w">
</span><span class="s2">"'Jaws'"</span><span class="p">,</span><span class="w">
</span><span class="mi">3</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="p">[</span><span class="w">
</span><span class="mi">2</span><span class="p">,</span><span class="w">
</span><span class="s2">"'Beastie Boys'"</span><span class="p">,</span><span class="w">
</span><span class="s2">"'Sabotage'"</span><span class="p">,</span><span class="w">
</span><span class="mi">4</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="p">[</span><span class="w">
</span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="s2">"'Hudson Mohawke'"</span><span class="p">,</span><span class="w">
</span><span class="s2">"'Cbat'"</span><span class="p">,</span><span class="w">
</span><span class="mi">5</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>Now that we can GET all songs, let’s build a function to GET just a single song. This function will take a parameter of <code class="highlighter-rouge">song_id</code>, create a connection to our database, find that song with a SQL query, and then return that song with JSON:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_single_song</span><span class="p">(</span><span class="n">song_id</span><span class="p">):</span>
<span class="k">with</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s">'songs.db'</span><span class="p">)</span> <span class="k">as</span> <span class="n">connection</span><span class="p">:</span>
<span class="n">cursor</span> <span class="o">=</span> <span class="n">connection</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">cursor</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s">"SELECT * FROM songs WHERE id = ?"</span><span class="p">,</span> <span class="p">(</span><span class="n">song_id</span><span class="p">,))</span>
<span class="n">song</span> <span class="o">=</span> <span class="n">cursor</span><span class="o">.</span><span class="n">fetchone</span><span class="p">()</span>
<span class="k">return</span> <span class="n">song</span>
</code></pre></div></div>
<p>We can update our route with a <code class="highlighter-rouge">song_id</code> as a parameter, and send back the single song:</p>
<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@app.route</span><span class="p">(</span><span class="s">'/api/song/<song_id>'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s">'GET'</span><span class="p">,</span> <span class="s">'PUT'</span><span class="p">,</span> <span class="s">'DELETE'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">resource</span><span class="p">(</span><span class="n">song_id</span><span class="p">):</span>
<span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'GET'</span><span class="p">:</span>
<span class="n">song</span> <span class="o">=</span> <span class="n">get_single_song</span><span class="p">(</span><span class="n">song_id</span><span class="p">)</span>
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">song</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'PUT'</span><span class="p">:</span>
<span class="k">pass</span> <span class="c"># Handle UPDATE request</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'DELETE'</span><span class="p">:</span>
<span class="k">pass</span> <span class="c"># Handle DELETE request</span>
</code></pre></div></div>
<p>If you now point your browser to <a href="http://127.0.0.1:5000/api/song/2">http://127.0.0.1:5000/api/song/2</a> you should see the JSON object for our song with an id of <code class="highlighter-rouge">2</code> in the database:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
</span><span class="mi">2</span><span class="p">,</span><span class="w">
</span><span class="s2">"'Beastie Boys'"</span><span class="p">,</span><span class="w">
</span><span class="s2">"'Sabotage'"</span><span class="p">,</span><span class="w">
</span><span class="mi">4</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>If you try to put in an id that we don’t have in the database currently, you will just get <code class="highlighter-rouge">null</code> displayed on the page instead of a JSON object.</p>
<p>We can CREATE a song, READ all songs, and READ a single song. Only two more routes to go…</p>
<h3 id="put">PUT</h3>
<p>A major function that we’re missing is the ability to edit data that’s already present in our database. We do this using a PUT request by taking incoming data with an id passed through the URL, finding the object in our database with that particular id, and then updating it.</p>
<p>Let’s start with an edit function, which takes in the song id, artist, title, and rating as arguments:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">edit_song</span><span class="p">(</span><span class="n">song_id</span><span class="p">,</span> <span class="n">artist</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">rating</span><span class="p">):</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">with</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s">'songs.db'</span><span class="p">)</span> <span class="k">as</span> <span class="n">connection</span><span class="p">:</span>
<span class="n">connection</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s">"UPDATE songs SET artist = ?, title = ?, rating = ? WHERE ID = ?;"</span><span class="p">,</span> <span class="p">(</span><span class="n">artist</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">rating</span><span class="p">,</span> <span class="n">song_id</span><span class="p">,))</span>
<span class="n">result</span> <span class="o">=</span> <span class="p">{</span><span class="s">'status'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">'message'</span><span class="p">:</span> <span class="s">'SONG Edited'</span><span class="p">}</span>
<span class="k">except</span><span class="p">:</span>
<span class="n">result</span> <span class="o">=</span> <span class="p">{</span><span class="s">'status'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s">'message'</span><span class="p">:</span> <span class="s">'Error'</span><span class="p">}</span>
<span class="k">return</span> <span class="n">result</span>
</code></pre></div></div>
<p>Now we can edit our route to pass in the data from the PUT request:</p>
<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@app.route</span><span class="p">(</span><span class="s">'/api/song/<song_id>'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s">'GET'</span><span class="p">,</span> <span class="s">'PUT'</span><span class="p">,</span> <span class="s">'DELETE'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">resource</span><span class="p">(</span><span class="n">song_id</span><span class="p">):</span>
<span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'GET'</span><span class="p">:</span>
<span class="n">song</span> <span class="o">=</span> <span class="n">get_single_song</span><span class="p">(</span><span class="n">song_id</span><span class="p">)</span>
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">song</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'PUT'</span><span class="p">:</span>
<span class="n">data</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">form</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">edit_song</span><span class="p">(</span>
<span class="n">song_id</span><span class="p">,</span> <span class="n">data</span><span class="p">[</span><span class="s">'artist'</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="s">'title'</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="s">'rating'</span><span class="p">])</span>
<span class="k">return</span> <span class="n">jsonify</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'DELETE'</span><span class="p">:</span>
<span class="k">pass</span> <span class="c"># Handle DELETE request</span>
</code></pre></div></div>
<p>So if we test this route out with CURL:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-X</span> PUT <span class="nt">--data</span> <span class="s2">"artist='Van Halen'&title='Hot for Teacher'&rating=3"</span> localhost:5000/api/song/2
</code></pre></div></div>
<p>We should see:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">{</span>
<span class="s2">"message"</span>: <span class="s2">"SONG Edited"</span>,
<span class="s2">"status"</span>: 1
<span class="o">}</span>
</code></pre></div></div>
<p>We can (err, should) make sure that edit is reflected in the database:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>sqlite3 songs.db
sqlite> SELECT <span class="k">*</span> FROM songs ORDER BY id desc<span class="p">;</span>
3|<span class="s1">'Gregori Klosman'</span>|<span class="s1">'Jaws'</span>|3
2|<span class="s1">'Van Halen'</span>|<span class="s1">'Hot for Teacher'</span>|3
1|<span class="s1">'Hudson Mohawke'</span>|<span class="s1">'Cbat'</span>|5
</code></pre></div></div>
<p>We can edit songs at will!</p>
<h3 id="delete">Delete</h3>
<p>The last thing we have left to do is our DELETE route. We need to be able to remove data from our database. Let’s first add in a song we can then delete using CURL in the terminal:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">--data</span> <span class="s2">"artist='The Flaming Lips'&title='Buggin'&rating=2"</span> http://localhost:5000/api/songs
</code></pre></div></div>
<p>Make sure it’s in our database:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>sqlite3 songs.db
sqlite> SELECT <span class="k">*</span> FROM songs ORDER BY id desc<span class="p">;</span>
4|<span class="s1">'The Flaming Lips'</span>|<span class="s1">'Buggin'</span>|2
3|<span class="s1">'Gregori Klosman'</span>|<span class="s1">'Jaws'</span>|3
2|<span class="s1">'Van Halen'</span>|<span class="s1">'Hot for Teacher'</span>|3
1|<span class="s1">'Hudson Mohawke'</span>|<span class="s1">'Cbat'</span>|5
</code></pre></div></div>
<p>We need to build a delete function:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">delete_song</span><span class="p">(</span><span class="n">song_id</span><span class="p">):</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">with</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s">'songs.db'</span><span class="p">)</span> <span class="k">as</span> <span class="n">connection</span><span class="p">:</span>
<span class="n">connection</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s">"DELETE FROM songs WHERE ID = ?;"</span><span class="p">,</span> <span class="p">(</span><span class="n">song_id</span><span class="p">,))</span>
<span class="n">result</span> <span class="o">=</span> <span class="p">{</span><span class="s">'status'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s">'message'</span><span class="p">:</span> <span class="s">'SONG Deleted'</span><span class="p">}</span>
<span class="k">except</span><span class="p">:</span>
<span class="n">result</span> <span class="o">=</span> <span class="p">{</span><span class="s">'status'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s">'message'</span><span class="p">:</span> <span class="s">'Error'</span><span class="p">}</span>
<span class="k">return</span> <span class="n">result</span>
</code></pre></div></div>
<p>And now let’s add in our delete route:</p>
<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@app.route</span><span class="p">(</span><span class="s">'/api/song/<song_id>'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s">'GET'</span><span class="p">,</span> <span class="s">'PUT'</span><span class="p">,</span> <span class="s">'DELETE'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">resource</span><span class="p">(</span><span class="n">song_id</span><span class="p">):</span>
<span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'GET'</span><span class="p">:</span>
<span class="n">song</span> <span class="o">=</span> <span class="n">get_single_song</span><span class="p">(</span><span class="n">song_id</span><span class="p">)</span>
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">song</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'PUT'</span><span class="p">:</span>
<span class="n">data</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">form</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">edit_song</span><span class="p">(</span>
<span class="n">song_id</span><span class="p">,</span> <span class="n">data</span><span class="p">[</span><span class="s">'artist'</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="s">'title'</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="s">'rating'</span><span class="p">])</span>
<span class="k">return</span> <span class="n">jsonify</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">request</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s">'DELETE'</span><span class="p">:</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">delete_song</span><span class="p">(</span><span class="n">song_id</span><span class="p">)</span>
<span class="k">return</span> <span class="n">jsonify</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
</code></pre></div></div>
<p>Test it with CURL:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-X</span> DELETE localhost:5000/api/song/4
<span class="o">{</span>
<span class="s2">"message"</span>: <span class="s2">"SONG Deleted"</span>,
<span class="s2">"status"</span>: 1
<span class="o">}</span>
</code></pre></div></div>
<p>And finally, go back into our database and really make sure it’s gone:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>sqlite3 songs.db
sqlite> SELECT <span class="k">*</span> FROM songs ORDER BY id desc<span class="p">;</span>
3|<span class="s1">'Gregori Klosman'</span>|<span class="s1">'Jaws'</span>|3
2|<span class="s1">'Van Halen'</span>|<span class="s1">'Hot for Teacher'</span>|3
1|<span class="s1">'Hudson Mohawke'</span>|<span class="s1">'Cbat'</span>|5
</code></pre></div></div>
<p>Boom! So we now have all of our routes doing <em>exactly</em> what we want them to do. We can add songs, get the songs back (all, or just a single song), edit a song, and remove a song. That’s some quality CRUD right there.</p>
<h2 id="next-steps">Next Steps</h2>
<ol>
<li><em>Error Handling</em>: The code we have right now is completely reliant on the data coming through correctly, but what if there’s something missing when the user sends a POST request? For example, what would happen if the artist name was missing? Right now we aren’t handling errors that may come up. Think about how we can send information back to the user if not all fields are present in the POST or PUT request, and how you could be clear in the error messages we send back to the user.</li>
<li><em>Server-side Templating</em>: Build out your client-side by adding <a href="http://flask.pocoo.org/docs/0.12/quickstart/#static-files">static files</a> and <a href="http://flask.pocoo.org/docs/0.12/quickstart/#rendering-templates">templates</a>.</li>
<li><em>Database Management:</em> Refactor SQLite and vanilla SQL out of your application and add in Postgres, <a href="http://flask-sqlalchemy.pocoo.org/2.1/">Flask-SQLAlchemy</a> (for communicating with the database), and <a href="https://flask-migrate.readthedocs.io/en/latest/">Flask-Migrate</a> (for migrations). Check out <a href="https://github.com/mjhea0/flask-songs-api/tree/master/_live">this example</a> of how to use Postgres and Flask-SQLAlchemy.</li>
</ol>
<p>Grab the code from the <a href="https://github.com/mjhea0/flask-songs-api">flask-songs-api</a> repo. Cheers!</p>
Michael Herman
Today we’ll be going through how to build a basic CRUD server-side application using Python and Flask, geared toward JavaScript developers versed in Node and Express. Similar to Express, Flask is a simple, yet powerful micro-framework for Python, perfect for RESTful APIs.
Developing and Testing Microservices with Docker
2017-04-18T00:00:00-05:00
2017-04-18T00:00:00-05:00
https://mherman.org/blog/developing-and-testing-microservices-with-docker
<p>Often, when developing applications with a microservice architecture, you cannot fully test out all services until you deploy to a staging server. This takes much too long to get feedback. Docker helps to speed up this process by making it easier to link together small, independent services locally.</p>
<p>In this article we’ll look at how to configure and test a number of services locally with <a href="https://docs.docker.com/">Docker</a> and <a href="https://docs.docker.com/compose/">Docker Compose</a>. We’ll also look at workflow and how to interact with and debug containers.</p>
<div style="text-align:center;">
<img src="/assets/img/blog/node-docker-api.png" style="max-width: 100%; border:0; box-shadow: none;" alt="microservice architecture" />
</div>
<p><br /></p>
<p>This post assumes prior knowledge of the following topics. Refer to the resources for more info:</p>
<table>
<thead>
<tr>
<th>Topic</th>
<th>Resource</th>
</tr>
</thead>
<tbody>
<tr>
<td>Docker</td>
<td><a href="https://docs.docker.com/engine/getstarted/">Get started with Docker</a></td>
</tr>
<tr>
<td>Docker Compose</td>
<td><a href="https://docs.docker.com/compose/gettingstarted/">Get started with Docker Compose</a></td>
</tr>
<tr>
<td>Node/Express API</td>
<td><a href="http://mherman.org/blog/2016/09/12/testing-node-and-express">Testing Node and Express</a></td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>NOTE</strong>: Looking for a more advanced implementation with React? Check out my other post - <a href="http://mherman.org/blog/2017/05/11/developing-microservices-node-react-docker">Developing Microservices - Node, React, and Docker</a>.</p>
</blockquote>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#docker-config" id="markdown-toc-docker-config">Docker Config</a></li>
<li><a href="#postgres-setup" id="markdown-toc-postgres-setup">Postgres Setup</a></li>
<li><a href="#users-service-setup" id="markdown-toc-users-service-setup">Users Service Setup</a></li>
<li><a href="#locations-service-setup" id="markdown-toc-locations-service-setup">Locations Service Setup</a></li>
<li><a href="#web-services-setup" id="markdown-toc-web-services-setup">Web Services Setup</a></li>
<li><a href="#testing" id="markdown-toc-testing">Testing</a></li>
<li><a href="#workflow" id="markdown-toc-workflow">Workflow</a></li>
<li><a href="#test-setup" id="markdown-toc-test-setup">Test Setup</a></li>
<li><a href="#next-steps" id="markdown-toc-next-steps">Next Steps</a></li>
</ul>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you should be able to…</p>
<ol>
<li>Configure and run a set of microservices locally with Docker and Docker Compose</li>
<li>Utilize <a href="https://docs.docker.com/engine/tutorials/dockervolumes/">volumes</a> to mount your code into a container</li>
<li>Run unit and integration tests inside a Docker container</li>
<li>Set up a separate container for functional tests</li>
<li>Debug a running Docker container</li>
<li>Utilize <a href="https://docs.docker.com/compose/compose-file/#links">links</a> for inter-container communication (AJAX)</li>
<li>Secure your services via JWT-based authentication</li>
</ol>
<h2 id="project-setup">Project Setup</h2>
<p>Start by cloning the base project and then checking out the first tag:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/mjhea0/node-docker-api
<span class="nv">$ </span><span class="nb">cd </span>node-docker-api
<span class="nv">$ </span>git checkout tags/v1
</code></pre></div></div>
<p>Take a quick look at the structure, broken down by service:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── services
│ ├── locations
│ │ ├── gulpfile.js
│ │ ├── knexfile.js
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── app.js
│ │ │ ├── db
│ │ │ │ ├── connection.js
│ │ │ │ ├── create.sql
│ │ │ │ ├── migrations
│ │ │ │ │ └── 20170405114746_locations.js
│ │ │ │ ├── queries.js
│ │ │ │ └── seeds
│ │ │ │ └── locations.js
│ │ │ ├── routes
│ │ │ │ ├── _helpers.js
│ │ │ │ └── locations.js
│ │ │ └── server.js
│ │ └── tests
│ │ └── integration
│ │ ├── routes.index.test.js
│ │ └── routes.locations.test.js
│ └── users
│ ├── gulpfile.js
│ ├── knexfile.js
│ ├── npm-debug.log
│ ├── package.json
│ ├── src
│ │ ├── app.js
│ │ ├── auth
│ │ │ ├── _helpers.js
│ │ │ └── local.js
│ │ ├── db
│ │ │ ├── connection.js
│ │ │ ├── create.sql
│ │ │ ├── migrations
│ │ │ │ └── 20170403223908_users.js
│ │ │ └── seeds
│ │ │ └── users.js
│ │ ├── routes
│ │ │ └── users.js
│ │ └── server.js
│ └── tests
│ ├── integration
│ │ ├── routes.index.test.js
│ │ └── routes.users.test.js
│ └── unit
│ ├── auth.helpers.test.js
│ └── auth.local.test.js
├── tests
│ ├── main.test.js
│ └── package.json
└── web
├── gulpfile.js
├── package.json
└── src
├── app.js
├── public
│ ├── main.css
│ └── main.js
├── routes
│ ├── _helpers.js
│ └── index.js
├── server.js
└── views
├── _base.html
├── error.html
├── login.html
├── main.html
├── nav.html
├── register.html
└── user.html
</code></pre></div></div>
<p>Before we Dockerize the services, feel free to test the locations and/or users services…</p>
<p>Users:</p>
<ol>
<li>Navigate to “services/users”</li>
<li><code class="highlighter-rouge">npm install</code></li>
<li><code class="highlighter-rouge">node src/server.js</code></li>
<li>Open <a href="http://localhost:3000/users/ping">http://localhost:3000/users/ping</a> in your browser</li>
</ol>
<p>Locations:</p>
<ol>
<li>Navigate to “services/locations”</li>
<li><code class="highlighter-rouge">npm install</code></li>
<li><code class="highlighter-rouge">node src/server.js</code></li>
<li>Open <a href="http://localhost:3001/locations/ping">http://localhost:3001/locations/ping</a> in your browser</li>
</ol>
<p>Kill the servers once done.</p>
<h2 id="docker-config">Docker Config</h2>
<p>Add a <em>docker-compose.yml</em> file to the project root, which is a config file used by Docker Compose to link multiple services together:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>version: '2.1'
</code></pre></div></div>
<blockquote>
<p><strong>NOTE:</strong> Why 2.1? <a href="https://docs.docker.com/compose/compose-file/compose-file-v2/#version-21">Answer</a>.</p>
</blockquote>
<p>Then add a <em>.dockerignore</em> to the “services/locations”, “services/locations/src/db”, “services/users”, “services/users/src/db”, “tests”, and “web” directories:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.git
.gitignore
README.md
docker-compose.yml
node_modules
</code></pre></div></div>
<p>With that, let’s set up each service individually, testing as we go…</p>
<h2 id="postgres-setup">Postgres Setup</h2>
<p>Add a <em>Dockerfile</em> to “services/locations/src/db” and “services/users/src/db”:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM postgres
# run create.sql on init
ADD create.sql /docker-entrypoint-initdb.d
</code></pre></div></div>
<p>Then update <em>docker-compose.yml</em>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>version: '2.1'
services:
users-db:
container_name: users-db
build: ./services/users/src/db
ports:
- '5433:5432'
environment:
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=admin
healthcheck:
test: exit 0
locations-db:
container_name: locations-db
build: ./services/locations/src/db
ports:
- '5434:5432'
environment:
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=admin
healthcheck:
test: exit 0
</code></pre></div></div>
<p>Here, we create two new containers called <code class="highlighter-rouge">users-db</code> and <code class="highlighter-rouge">locations-db</code>, from the <em>Dockerfiles</em> found in “services/users/src/db” and “services/locations/src/db”, respectively. We also add environment variables, expose ports, and send an exit code <code class="highlighter-rouge">0</code> once they are successfully up and running - which will be used by other services.</p>
<p>To fire up the containers, run:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">--build</span> <span class="nt">-d</span>
</code></pre></div></div>
<p>Once up, you can get a quick sanity check, by entering the shell:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose run users-db bash
<span class="c"># exit</span>
<span class="nv">$ </span>docker-compose run locations-db bash
<span class="c"># exit</span>
</code></pre></div></div>
<h2 id="users-service-setup">Users Service Setup</h2>
<p>Again, add a <em>Dockerfile</em> to “services/users”, making sure to review the comments:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM node:latest
# set working directory
RUN mkdir /src
WORKDIR /src
# install app dependencies
ENV PATH /src/node_modules/.bin:$PATH
ADD package.json /src/package.json
RUN npm install
# start app
CMD ["npm", "start"]
</code></pre></div></div>
<p>Add the <code class="highlighter-rouge">users-service</code> to the <em>docker-compose.yml</em> file:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>users-service:
container_name: users-service
build: ./services/users/
volumes:
- './services/users:/src/app'
- './services/users/package.json:/src/package.json'
ports:
- '3000:3000'
environment:
- DATABASE_URL=postgres://admin:admin@users-db:5432/node_docker_api_users_dev
- DATABASE_TEST_URL=postgres://admin:admin@users-db:5432/node_docker_api_users_test
- NODE_ENV=${NODE_ENV}
- TOKEN_SECRET=changeme
depends_on:
users-db:
condition: service_healthy
links:
- users-db
</code></pre></div></div>
<p>What’s new here?</p>
<ol>
<li><code class="highlighter-rouge">volumes</code>: <a href="https://docs.docker.com/engine/tutorials/dockervolumes/">volumes</a> are used to mount a directory into a container so that you can make changes to the code without having to build a new image. This should be a default in your local development environment so you can get quick feedback on code changes.</li>
<li><code class="highlighter-rouge">depends_on</code>: <a href="https://docs.docker.com/compose/compose-file/#dependson">depends_on</a> is used to start services in a specific order. So, the <code class="highlighter-rouge">users-service</code> will wait for the <code class="highlighter-rouge">users-db</code> to fire up successfully (with an exit code of <code class="highlighter-rouge">0</code>) before it starts.</li>
<li><code class="highlighter-rouge">links</code>: With <a href="https://docs.docker.com/compose/compose-file/#links">links</a>, code inside the <code class="highlighter-rouge">users-service</code> can access the database via <code class="highlighter-rouge">users-db:5432</code>.</li>
</ol>
<p>Set the <code class="highlighter-rouge">NODE_ENV</code> environment variable:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">export </span><span class="nv">NODE_ENV</span><span class="o">=</span>development
</code></pre></div></div>
<p>Spin up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">--build</span> <span class="nt">-d</span> users-service
</code></pre></div></div>
<p>Once up, create a new file in the project root called <em>migrate.sh</em> and add the Knex migrate and seed commands:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>
docker-compose run users-service knex migrate:latest <span class="nt">--env</span> development <span class="nt">--knexfile</span> app/knexfile.js
docker-compose run users-service knex seed:run <span class="nt">--env</span> development <span class="nt">--knexfile</span> app/knexfile.js
</code></pre></div></div>
<p>Then run it:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>sh migrate.sh
</code></pre></div></div>
<p>Test:</p>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>HTTP Method</th>
<th>CRUD Method</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>/users/ping</td>
<td>GET</td>
<td>READ</td>
<td><code class="highlighter-rouge">pong</code></td>
</tr>
<tr>
<td>/users/register</td>
<td>POST</td>
<td>CREATE</td>
<td>add a user</td>
</tr>
<tr>
<td>/users/login</td>
<td>POST</td>
<td>CREATE</td>
<td>log in a user</td>
</tr>
<tr>
<td>/users/user</td>
<td>GET</td>
<td>READ</td>
<td>get user info</td>
</tr>
</tbody>
</table>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>http POST http://localhost:3000/users/register <span class="nv">username</span><span class="o">=</span>michael <span class="nv">password</span><span class="o">=</span>herman
<span class="nv">$ </span>http POST http://localhost:3000/users/login <span class="nv">username</span><span class="o">=</span>michael <span class="nv">password</span><span class="o">=</span>herman
</code></pre></div></div>
<blockquote>
<p><strong>NOTE:</strong> <code class="highlighter-rouge">http</code> in the above commands is part of the <a href="https://httpie.org/">HTTPie</a> library, which is a wrapper on top of cURL.</p>
</blockquote>
<h2 id="locations-service-setup">Locations Service Setup</h2>
<p>Add the <em>Dockerfile</em>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM node:latest
# set working directory
RUN mkdir /src
WORKDIR /src
# install app dependencies
ENV PATH /src/node_modules/.bin:$PATH
ADD package.json /src/package.json
RUN npm install
# start app
CMD ["npm", "start"]
</code></pre></div></div>
<p>Add the service to <em>docker-compose</em>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>locations-service:
container_name: locations-service
build: ./services/locations/
volumes:
- './services/locations:/src/app'
- './services/locations/package.json:/src/package.json'
ports:
- '3001:3001'
environment:
- DATABASE_URL=postgres://admin:admin@locations-db:5432/node_docker_api_locations_dev
- DATABASE_TEST_URL=postgres://admin:admin@locations-db:5432/node_docker_api_locations_test
- NODE_ENV=${NODE_ENV}
- TOKEN_SECRET=changeme
- OPENWEATHERMAP_API_KEY=${OPENWEATHERMAP_API_KEY}
depends_on:
locations-db:
condition: service_healthy
users-service:
condition: service_started
links:
- locations-db
- users-service
</code></pre></div></div>
<p>Register with the <a href="https://openweathermap.org/api">OpenWeatherMap API</a>, and add the key as an environment variable:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">export </span><span class="nv">OPENWEATHERMAP_API_KEY</span><span class="o">=</span>YOUR_KEY_HERE
</code></pre></div></div>
<p>Spin up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">--build</span> <span class="nt">-d</span> locations-service
</code></pre></div></div>
<p>Add the migrate and seed commands to <em>migrate.sh</em>:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose run locations-service knex migrate:latest <span class="nt">--env</span> development <span class="nt">--knexfile</span> app/knexfile.js
docker-compose run locations-service knex seed:run <span class="nt">--env</span> development <span class="nt">--knexfile</span> app/knexfile.js
</code></pre></div></div>
<p>Run it:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>sh migrate.sh
</code></pre></div></div>
<p>Test:</p>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>HTTP Method</th>
<th>CRUD Method</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>/locations/ping</td>
<td>GET</td>
<td>READ</td>
<td><code class="highlighter-rouge">pong</code></td>
</tr>
<tr>
<td>/locations</td>
<td>GET</td>
<td>READ</td>
<td>get all locations</td>
</tr>
<tr>
<td>/locations/user</td>
<td>GET</td>
<td>READ</td>
<td>get all locations by user</td>
</tr>
<tr>
<td>/locations/:id</td>
<td>GET</td>
<td>READ</td>
<td>get a single location</td>
</tr>
<tr>
<td>/locations</td>
<td>POST</td>
<td>CREATE</td>
<td>add a single location</td>
</tr>
<tr>
<td>/locations/:id</td>
<td>PUT</td>
<td>UPDATE</td>
<td>update a single location</td>
</tr>
<tr>
<td>/locations/:id</td>
<td>DELETE</td>
<td>DELETE</td>
<td>delete a single location</td>
</tr>
</tbody>
</table>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>http GET http://localhost:3001/locations/ping
</code></pre></div></div>
<h2 id="web-services-setup">Web Services Setup</h2>
<p>Moving right along…</p>
<p>Add the <em>Dockerfile</em>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM node:latest
# set working directory
RUN mkdir /src
WORKDIR /src
# install app dependencies
ENV PATH /src/node_modules/.bin:$PATH
ADD package.json /src/package.json
RUN npm install
# start app
CMD ["npm", "start"]
</code></pre></div></div>
<p>Add the service to <em>docker-compose</em>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>web:
container_name: web
build: ./web/
volumes:
- './web:/src/app'
- './web/package.json:/src/package.json'
ports:
- '3003:3003'
environment:
- NODE_ENV=${NODE_ENV}
- SECRET_KEY=changeme
depends_on:
users-service:
condition: service_started
locations-service:
condition: service_started
links:
- users-service
- locations-service
</code></pre></div></div>
<p>Spin up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">--build</span> <span class="nt">-d</span> web
</code></pre></div></div>
<p>Navigate to <a href="http://localhost:3003">http://localhost:3003</a> in your browser and you should see the login page. Register a new user. Once redirected, you should see:</p>
<div style="text-align:center;">
<img src="/assets/img/blog/node-docker-api-browser.png" style="max-width: 100%; border:0; box-shadow: none;" alt="node docker browser view" />
</div>
<p><br /></p>
<p>Take a look at the AJAX request in the GET <code class="highlighter-rouge">/</code> route in <em>web/src/routes/index.js</em>. Why does the <code class="highlighter-rouge">uri</code> point to <code class="highlighter-rouge">locations-service</code> and not <code class="highlighter-rouge">localhost</code>? Well, <code class="highlighter-rouge">localhost</code> refers back to the container itself, so you need to set up a <a href="https://docs.docker.com/compose/compose-file/#links">link</a> in the Docker compose - which we’ve already done.</p>
<h2 id="testing">Testing</h2>
<p>Did you notice the unit and integration tests in the “services/users/tests” and “services/locations/tests” folders? Well, to run the tests properly, we need to update the <code class="highlighter-rouge">NODE_ENV</code> environment variable, since it is currently configured for the development environment.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">export </span><span class="nv">NODE_ENV</span><span class="o">=</span><span class="nb">test</span>
</code></pre></div></div>
<p>Update the containers:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">-d</span>
</code></pre></div></div>
<p>Then run the tests:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose run users-service npm <span class="nb">test</span>
<span class="nv">$ </span>docker-compose run locations-service npm <span class="nb">test</span>
</code></pre></div></div>
<p>Ready to develop again?</p>
<ol>
<li>Update the env variable - <code class="highlighter-rouge">export NODE_ENV=development</code></li>
<li>Update the containers - <code class="highlighter-rouge">docker-compose up -d</code></li>
</ol>
<h2 id="workflow">Workflow</h2>
<p>Let’s quickly look at how to work with code inside the containers…</p>
<ol>
<li>Live Reloading - Since the code is mounted in the container via a volume, you can make changes to the local code base which will be applied to the code in the container. <a href="https://github.com/remy/nodemon">Nodemon</a> is used (along with Gulp) to restart the app when changes occur.</li>
<li>Debugging - <code class="highlighter-rouge">console.log</code> can be used for testing and debugging. Simply add one to your code base and then open the logs…</li>
<li>Logs - run <code class="highlighter-rouge">docker-compose logs -f</code> to view the logs.</li>
</ol>
<blockquote>
<p><strong>NOTE:</strong> Check out the <a href="https://github.com/mjhea0/node-docker-api">repo</a> to view more commands.</p>
</blockquote>
<p>Try it out:</p>
<ol>
<li>Run <code class="highlighter-rouge">docker-compose logs -f</code> in the terminal</li>
<li>Add <code class="highlighter-rouge">console.log('here');</code> to the top of <em>web/src/routes/index.js</em></li>
<li>
<p>As soon as you save, you should see the following in the terminal:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> web | <span class="o">[</span>18:35:37] <span class="o">[</span>nodemon] restarting due to changes...
web | <span class="o">[</span>18:35:37] <span class="o">[</span>nodemon] running tasks...
web | <span class="o">[</span>18:35:39] Using gulpfile /src/app/gulpfile.js
web | <span class="o">[</span>18:35:39] Starting <span class="s1">'lint'</span>...
web | <span class="o">[</span>18:35:40]
web | /src/app/src/routes/index.js
web | 1:1 warning Unexpected console statement no-console
web |
web | ✖ 1 problem <span class="o">(</span>0 errors, 1 warning<span class="o">)</span>
web |
web | <span class="o">[</span>18:35:40] Finished <span class="s1">'lint'</span> after 1.69 s
web | <span class="o">[</span>18:35:40] <span class="o">[</span>nodemon] starting <span class="sb">`</span>node ./src/server<span class="sb">`</span>
web | here
</code></pre></div> </div>
<p>Essentially, Nodemon detected changes and restarted the app, which fired the linter and then the server fired back up.</p>
</li>
</ol>
<h2 id="test-setup">Test Setup</h2>
<p>Finally, to set up the last service, add the <em>Dockerfile</em> to the “tests” folder:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM node:latest
# set working directory
RUN mkdir /src
WORKDIR /src
# install app dependencies
ENV PATH /src/node_modules/.bin:$PATH
ADD package.json /src/package.json
RUN npm install
</code></pre></div></div>
<p>Then update the <em>docker-compose.yml</em> file:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tests:
container_name: tests
build: ./tests/
volumes:
- './tests:/src/app'
- './tests/package.json:/src/package.json'
depends_on:
users-service:
condition: service_started
locations-service:
condition: service_started
links:
- users-service
- locations-service
- web
</code></pre></div></div>
<p>Fire up the container:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker-compose up <span class="nt">--build</span> <span class="nt">-d</span> tests
</code></pre></div></div>
<p>Update the environment variable, update the containers, and then run the tests:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">export </span><span class="nv">NODE_ENV</span><span class="o">=</span><span class="nb">test</span>
<span class="nv">$ </span>docker-compose up <span class="nt">-d</span>
<span class="nv">$ </span>docker-compose run tests npm <span class="nb">test</span>
</code></pre></div></div>
<h2 id="next-steps">Next Steps</h2>
<p>What’s next?</p>
<ol>
<li><strong>Dependency management</strong>: Right now we’re installing many of the same dependencies over and over again, in multiple containers. How can we manage this better to spin up new containers faster and save disc space? How about a data-only container that just houses dependencies?</li>
<li><strong>Deployment prep</strong>: Set up Docker Machine for spinning up Docker environments, nginx for load balancing, and Consul for service discovery. Update the environment variables for the base URL since these will be different in production. Add an image registry solution and a data-only container for piping logs to…</li>
<li><strong>Error handling</strong>: Right now errors are being thrown, but there really isn’t much info in the response as to why, which makes debugging difficult. Be a good citizen and handle your errors properly since you may not always have access to the code base from a different service.</li>
<li><strong>DRY</strong>: The code could be refactored in places, especially the tests.</li>
</ol>
<p>Grab the final code from the <a href="https://github.com/mjhea0/node-docker-api">node-docker-api</a> repo. Comment below. Cheers!</p>
Michael Herman
Often, when developing applications with a microservice architecture, you cannot fully test out all services until you deploy to a staging server. This takes much too long to get feedback. Docker helps to speed up this process by making it easier to link together small, independent services locally.
Functional Testing with TestCafe
2017-03-19T00:00:00-05:00
2017-03-19T00:00:00-05:00
https://mherman.org/blog/functional-testing-with-testcafe
<p>Today we are going to dive into the world of functional web testing with <a href="https://devexpress.github.io/testcafe/">TestCafe</a>.</p>
<p>Unlike the majority of other end-to-end (e2e) testing tools, TestCafe is not dependent on Selenium or WebDriver. Instead, it injects scripts into the browser to communicate directly with the DOM and handle events. It works on any modern browser that supports HTML5 without any plugins. Further, it supports all major operating systems and can run simultaneously on multiple browsers and machines.</p>
<div style="text-align:center;">
<img src="/assets/img/blog/testcafe.png" style="max-width: 500px; border:0; box-shadow: none;" alt="testcafe logo" />
</div>
<p><br /></p>
<p>We will be using:</p>
<ul>
<li>TestCafe v<a href="https://github.com/DevExpress/testcafe/releases/tag/v0.13.0">0.13.0</a></li>
<li>Chrome v<a href="https://chromereleases.googleblog.com/2017/03/stable-channel-update-for-desktop.html">57</a></li>
<li>NodeJS v<a href="https://nodejs.org/docs/v7.6.0/api/all.html">7.6.0</a></li>
</ul>
<p>Please review the <a href="http://devexpress.github.io/testcafe/documentation/getting-started/">Getting Started</a> guide before beginning.</p>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#contents-1" id="markdown-toc-contents-1">Contents</a></li>
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#writing-tests" id="markdown-toc-writing-tests">Writing Tests</a></li>
<li><a href="#browser-support" id="markdown-toc-browser-support">Browser Support</a></li>
<li><a href="#continuous-integration" id="markdown-toc-continuous-integration">Continuous Integration</a></li>
</ul>
<h2 id="contents-1">Contents</h2>
<ol>
<li>Objectives</li>
<li>Project Setup</li>
<li>Writing Tests</li>
<li>Browser Support</li>
<li>Continuous Integration</li>
<li>Next Steps</li>
</ol>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you should be able to…</p>
<ol>
<li>Set up TestCafe with an existing Node app</li>
<li>Write TestCafe tests using the <a href="https://martinfowler.com/bliki/PageObject.html">PageObject</a> pattern</li>
<li>Test a Node application with functional tests</li>
<li>Integrate TestCafe into a continuous integration process</li>
<li>Configure TestCafe to work with a headless browser</li>
</ol>
<h2 id="project-setup">Project Setup</h2>
<p>Start by cloning the base project structure:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/mjhea0/testcafe-example <span class="nt">--branch</span> v1 <span class="nt">--single-branch</span> <span class="nt">-b</span> master
</code></pre></div></div>
<p>Install the dependencies, and then fire up the app by running <code class="highlighter-rouge">npm start</code> to make sure all is well. Navigate to <a href="http://localhost:3000/">http://localhost:3000/</a> in your browser and you should see a list of jobs in HTML. Experiment with the app. Add a job. Update a job. Delete a job. This is what we will be testing. Kill the server when done.</p>
<div style="text-align:center;">
<img src="/assets/img/blog/node-jobs.png" style="max-width: 100%; border:0; box-shadow: none;" alt="node jobs" />
</div>
<p>Install TestCafe:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install testcafe@0.13.0 <span class="nt">--save-dev</span>
</code></pre></div></div>
<p>With that, you can start running tests.</p>
<blockquote>
<p><strong>NOTE:</strong> If you were using a Selenium-based testing tool you would need to install both Selenium and Web Driver, which can be difficult depending on your system setup.</p>
</blockquote>
<p>Add a <code class="highlighter-rouge">test</code> command to the <code class="highlighter-rouge">scripts</code> in <em>package.json</em>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"start"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node ./bin/www"</span><span class="p">,</span><span class="w">
</span><span class="s2">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node_modules/testcafe/bin/testcafe.js chrome tests/"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span></code></pre></div></div>
<p>Here, we specified the path to TestCafe in our “node_modules” folder along with a <a href="http://devexpress.github.io/testcafe/documentation/using-testcafe/command-line-interface.html#browser-list">target browser</a>, <code class="highlighter-rouge">chrome</code>, and a <a href="http://devexpress.github.io/testcafe/documentation/using-testcafe/command-line-interface.html#file-pathglob-pattern">path</a> to where all tests will be located, <code class="highlighter-rouge">tests/</code>.</p>
<p>Now, you can use <code class="highlighter-rouge">npm test</code> to run TestCafe.</p>
<p>Let’s get a test set up. Add a “tests” folder to the project root, and add an <em>index.js</em> file to it:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Selector</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'testcafe'</span><span class="p">;</span>
<span class="nx">fixture</span><span class="p">(</span><span class="s1">'Getting Started'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">page</span><span class="p">(</span><span class="s1">'https://github.com'</span><span class="p">);</span>
<span class="nx">test</span><span class="p">(</span><span class="s1">'Find "testcafe-example" repo on GitHub'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">t</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">repo</span> <span class="o">=</span> <span class="nx">Selector</span><span class="p">(</span><span class="s1">'.repo-list > li > div'</span><span class="p">);</span>
<span class="c1">// search github</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">typeText</span><span class="p">(</span><span class="s1">'form[action="/search"]'</span><span class="p">,</span> <span class="s1">'testcafe-example user:mjhea0'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">pressKey</span><span class="p">(</span><span class="s1">'enter'</span><span class="p">);</span>
<span class="c1">// check li for results</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">repo</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">contains</span><span class="p">(</span><span class="s1">'mjhea0/testcafe-example'</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>What’s happening?</p>
<ol>
<li>Since <em>all</em> tests are organized into <a href="http://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#fixtures">fixtures</a>, we started with a <code class="highlighter-rouge">fixture()</code> function.</li>
<li>Next, we specified a start URL - <code class="highlighter-rouge">http://devexpress.github.io/testcafe/example</code> - via the <code class="highlighter-rouge">page()</code> <a href="http://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#specifying-the-start-webpage">method</a>.</li>
<li>From there, we added the test code into a <code class="highlighter-rouge">test()</code> <a href="http://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#tests">function</a>, which takes an async function along with the <a href="http://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#test-controller">test controller</a> object.</li>
<li><code class="highlighter-rouge">await</code> is then used to wait for certain <a href="http://devexpress.github.io/testcafe/documentation/test-api/actions/">actions</a> to complete. In this case, we used <code class="highlighter-rouge">typeText()</code> and <code class="highlighter-rouge">pressKey()</code> to search GitHub.</li>
<li>On the GitHub search results page, we used a <code class="highlighter-rouge">Selector()</code> <a href="http://devexpress.github.io/testcafe/documentation/test-api/selecting-page-elements/selectors.html">function</a> to parse the DOM.</li>
<li>Finally, we asserted that the actual results contain the expected results.</li>
</ol>
<blockquote>
<p><strong>NOTE:</strong> If you’re new to <a href="https://github.com/tc39/ecmascript-asyncawait">async/await</a>, check out <a href="https://ponyfoo.com/articles/understanding-javascript-async-await">Understanding JavaScript’s async await</a>.</p>
</blockquote>
<p>Try this out! Run <code class="highlighter-rouge">npm test</code>. If all goes well Chrome should fire up and execute the test. Once done, you should see something like this in your terminal:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Running tests <span class="k">in</span>:
- Chrome 57.0.2987 / Mac OS X 10.11.6
Getting Started
✓ Find <span class="s2">"testcafe-example"</span> repo on GitHub
</code></pre></div></div>
<p>Make sense? No? Continue to run the test and review the above steps until it does. Make sure you understand what’s happening before moving on.</p>
<h2 id="writing-tests">Writing Tests</h2>
<p>Add a new file called <em>jobs.js</em> to the “tests” folder:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Selector</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'testcafe'</span><span class="p">;</span>
<span class="nx">fixture</span><span class="p">(</span><span class="s1">'Node Jobs'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">page</span><span class="p">(</span><span class="s1">'http://localhost:3000'</span><span class="p">);</span>
<span class="nx">test</span><span class="p">(</span><span class="s1">'All Jobs'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">t</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Then update the <code class="highlighter-rouge">test</code> command in <em>package.json</em>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node_modules/testcafe/bin/testcafe.js chrome tests/jobs.js --app 'npm start'"</span><span class="w">
</span></code></pre></div></div>
<p><code class="highlighter-rouge">tests/jobs.js</code> ignores the example GitHub test found in <em>index.js</em> so that we can focus just on the tests added to <em>jobs.js</em>. The <code class="highlighter-rouge">--app</code> <a href="https://devexpress.github.io/testcafe/documentation/using-testcafe/command-line-interface.html#-a-command---app-command">option</a> is used to launch the Node app so that TestCafe can interact with it.</p>
<p>Try it. You should see the page load in Chrome. With that, let’s test each of our app’s CRUD functions.</p>
<h3 id="get-all-jobs">GET ALL Jobs</h3>
<p>Update <em>jobs.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Selector</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'testcafe'</span><span class="p">;</span>
<span class="nx">fixture</span><span class="p">(</span><span class="s1">'Node Jobs'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">page</span><span class="p">(</span><span class="s1">'http://localhost:3000'</span><span class="p">);</span>
<span class="nx">test</span><span class="p">(</span><span class="s1">'All Jobs'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">t</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">Selector</span><span class="p">(</span><span class="s1">'h1'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">tableRows</span> <span class="o">=</span> <span class="nx">Selector</span><span class="p">(</span><span class="s1">'tbody > tr'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">addJobButton</span> <span class="o">=</span> <span class="nx">Selector</span><span class="p">(</span><span class="s1">'a.btn.btn-primary'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">firstJob</span> <span class="o">=</span> <span class="nx">Selector</span><span class="p">(</span><span class="s1">'tbody > tr'</span><span class="p">).</span><span class="nx">withText</span><span class="p">(</span><span class="s1">'Horse Whisperer'</span><span class="p">);</span>
<span class="c1">// check title, add job button, table rows, and job exists</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">title</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'All Jobs'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">addJobButton</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'Add New Job'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">tableRows</span><span class="p">.</span><span class="nx">count</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">firstJob</span><span class="p">.</span><span class="nx">exists</span><span class="p">).</span><span class="nx">ok</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>What’s happening? Review the code above, line by line. It should be fairly straightforward. Turn to the <a href="https://devexpress.github.io/testcafe/documentation/test-api/selecting-page-elements/">docs</a> for help, adding comments as necessary.</p>
<p>Run:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Node Jobs
✓ All Jobs
1 passed <span class="o">(</span>0s<span class="o">)</span>
</code></pre></div></div>
<p>Before moving on, refactor out the selectors so that they can be re-used by other test cases:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Selector</span> <span class="p">}</span> <span class="k">from</span> <span class="s1">'testcafe'</span><span class="p">;</span>
<span class="c1">// selectors</span>
<span class="kd">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">Selector</span><span class="p">(</span><span class="s1">'h1'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">tableRows</span> <span class="o">=</span> <span class="nx">Selector</span><span class="p">(</span><span class="s1">'tbody > tr'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">addJobButton</span> <span class="o">=</span> <span class="nx">Selector</span><span class="p">(</span><span class="s1">'a.btn.btn-primary'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">firstJob</span> <span class="o">=</span> <span class="nx">Selector</span><span class="p">(</span><span class="s1">'tbody > tr'</span><span class="p">).</span><span class="nx">withText</span><span class="p">(</span><span class="s1">'Horse Whisperer'</span><span class="p">);</span>
<span class="nx">fixture</span><span class="p">(</span><span class="s1">'Node Jobs'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">page</span><span class="p">(</span><span class="s1">'http://localhost:3000'</span><span class="p">);</span>
<span class="nx">test</span><span class="p">(</span><span class="s1">'All Jobs'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">t</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// check title, add job button, table rows, and job exists</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">title</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'All Jobs'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">addJobButton</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'Add New Job'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">tableRows</span><span class="p">.</span><span class="nx">count</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">firstJob</span><span class="p">.</span><span class="nx">exists</span><span class="p">).</span><span class="nx">ok</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<h3 id="add-job">Add Job</h3>
<p>Start by adding a new <code class="highlighter-rouge">test()</code> function to <em>jobs.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">test</span><span class="p">.</span><span class="nx">only</span><span class="p">(</span><span class="s1">'New Job'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">t</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">});</span>
</code></pre></div></div>
<blockquote>
<p><strong>NOTE:</strong> Can you guess what <code class="highlighter-rouge">only()</code> does? Try running the tests to see. Please review the <a href="https://devexpress.github.io/testcafe/documentation/test-api/test-code-structure.html#skipping-tests">docs</a> for more info.</p>
</blockquote>
<p>Think about the steps an end user has to go through to add a job:</p>
<ol>
<li>Click the add job button</li>
<li>Fill out the form</li>
<li>Submit the form</li>
</ol>
<p>Now, try this on your own, step by step, before looking at the solution…</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">test</span><span class="p">.</span><span class="nx">only</span><span class="p">(</span><span class="s1">'New Job'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">t</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// click add job button</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">addJobButton</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">title</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'Add Job'</span><span class="p">);</span>
<span class="c1">// fill out form</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">typeText</span><span class="p">(</span><span class="s1">'input[name="title"]'</span><span class="p">,</span> <span class="s1">'Python Developer'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">typeText</span><span class="p">(</span><span class="s1">'textarea[name="description"]'</span><span class="p">,</span> <span class="s1">'Write some Python'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">typeText</span><span class="p">(</span><span class="s1">'input[name="company"]'</span><span class="p">,</span> <span class="s1">'Real Python'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">typeText</span><span class="p">(</span><span class="s1">'input[name="email"]'</span><span class="p">,</span> <span class="s1">'michael@realpython.com'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">submitButton</span><span class="p">)</span>
<span class="c1">// check title, table rows, and new job exists</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">title</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'All Jobs'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">tableRows</span><span class="p">.</span><span class="nx">count</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">Selector</span><span class="p">(</span><span class="s1">'tbody > tr'</span><span class="p">).</span><span class="nx">withText</span><span class="p">(</span><span class="s1">'Python Developer'</span><span class="p">).</span><span class="nx">exists</span><span class="p">).</span><span class="nx">ok</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Make sure to add the selector to the top:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">submitButton</span> <span class="o">=</span> <span class="nx">Selector</span><span class="p">(</span><span class="s1">'button[type="submit"]'</span><span class="p">);</span>
</code></pre></div></div>
<p>Test it out. Then remove the <code class="highlighter-rouge">only()</code> and test again:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Node Jobs
✓ All Jobs
✓ New Job
2 passed <span class="o">(</span>4s<span class="o">)</span>
</code></pre></div></div>
<p>What are we missing in this test?</p>
<ol>
<li>What happens if the cancel button is pressed?</li>
<li>What if the end user does not enter data for all the fields?</li>
<li>What if text is entered in the email field but it is not a valid email?</li>
</ol>
<p>Try testing for these on your own.</p>
<h3 id="update-job">Update Job</h3>
<p>Again, start by adding the boilerplate:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">test</span><span class="p">(</span><span class="s1">'Update Job'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">t</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Then write out the steps the end user has to take before writing any code:</p>
<ol>
<li>Click the update button</li>
<li>Fill out the form</li>
<li>Submit the form</li>
</ol>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">test</span><span class="p">(</span><span class="s1">'Update Job'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">t</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// click update button</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">firstJob</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">'a.btn.btn-warning'</span><span class="p">))</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">title</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'Update Job'</span><span class="p">);</span>
<span class="c1">// fill out form</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">typeText</span><span class="p">(</span><span class="s1">'input[name="title"]'</span><span class="p">,</span> <span class="s1">'testing an update'</span><span class="p">,</span> <span class="p">{</span><span class="na">replace</span><span class="p">:</span> <span class="kc">true</span><span class="p">})</span>
<span class="p">.</span><span class="nx">typeText</span><span class="p">(</span><span class="s1">'textarea[name="description"]'</span><span class="p">,</span> <span class="s1">'test'</span><span class="p">,</span> <span class="p">{</span><span class="na">replace</span><span class="p">:</span> <span class="kc">true</span><span class="p">})</span>
<span class="p">.</span><span class="nx">typeText</span><span class="p">(</span><span class="s1">'input[name="company"]'</span><span class="p">,</span> <span class="s1">'test'</span><span class="p">,</span> <span class="p">{</span><span class="na">replace</span><span class="p">:</span> <span class="kc">true</span><span class="p">})</span>
<span class="p">.</span><span class="nx">typeText</span><span class="p">(</span><span class="s1">'input[name="email"]'</span><span class="p">,</span> <span class="s1">'t@t.com'</span><span class="p">,</span> <span class="p">{</span><span class="na">replace</span><span class="p">:</span> <span class="kc">true</span><span class="p">})</span>
<span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">submitButton</span><span class="p">)</span>
<span class="c1">// check title, table rows, and updated job exists</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">title</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'All Jobs'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">tableRows</span><span class="p">.</span><span class="nx">count</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span> <span class="c1">// why 4?</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">firstJob</span><span class="p">.</span><span class="nx">exists</span><span class="p">).</span><span class="nx">notOk</span><span class="p">()</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">Selector</span><span class="p">(</span><span class="s1">'tbody > tr'</span><span class="p">).</span><span class="nx">withText</span><span class="p">(</span><span class="s1">'testing an update'</span><span class="p">).</span><span class="nx">exists</span><span class="p">).</span><span class="nx">ok</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Test:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Node Jobs
✓ All Jobs
✓ New Job
✓ Update Job
3 passed <span class="o">(</span>8s<span class="o">)</span>
</code></pre></div></div>
<p>What else should you test for? Write the test cases on your own.</p>
<p>Also, did you notice the code smell? There’s a lot of code duplication happening between those last two test cases. How could this be better handled?</p>
<p>Finally, did you notice that there are still four jobs in the table? Why? Could there be issues with testing the previous two tests together rather than in isolation? Probably not in this case, but if there are, you could always wrap the update in a new <code class="highlighter-rouge">fixture()</code>, since this restores the page to its initial state.</p>
<h3 id="delete-job">Delete Job</h3>
<p>Run the app again with <code class="highlighter-rouge">npm start</code> to review, from the end user’s perspective, what happens when you try to delete a job.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">test</span><span class="p">(</span><span class="s1">'Delete Job'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">t</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// click delete button</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">setNativeDialogHandler</span><span class="p">(()</span> <span class="o">=></span> <span class="kc">true</span><span class="p">)</span>
<span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">clayDryerJob</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">'a.btn.btn-danger'</span><span class="p">))</span>
<span class="c1">// check title, table rows, and updated job exists</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">title</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'All Jobs'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">tableRows</span><span class="p">.</span><span class="nx">count</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span> <span class="c1">// why 3?</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">clayDryerJob</span><span class="p">.</span><span class="nx">exists</span><span class="p">).</span><span class="nx">notOk</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Did you notice the <code class="highlighter-rouge">setNativeDialogHandler()</code> function? <a href="https://devexpress.github.io/testcafe/documentation/test-api/handling-native-dialogs.html#dialog-handler">This</a> tells TestCafe how to handle the alert.</p>
<p>What if we click “cancel” instead of “ok”?</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">test</span><span class="p">(</span><span class="s1">'Delete Job'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">t</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// click delete button</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">setNativeDialogHandler</span><span class="p">(()</span> <span class="o">=></span> <span class="kc">true</span><span class="p">)</span> <span class="c1">// => press ok</span>
<span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">clayDryerJob</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">'a.btn.btn-danger'</span><span class="p">))</span>
<span class="c1">// check title, table rows, and updated job exists</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">title</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'All Jobs'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">tableRows</span><span class="p">.</span><span class="nx">count</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span> <span class="c1">// why 3?</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">clayDryerJob</span><span class="p">.</span><span class="nx">exists</span><span class="p">).</span><span class="nx">notOk</span><span class="p">();</span>
<span class="c1">// click delete button</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">setNativeDialogHandler</span><span class="p">(()</span> <span class="o">=></span> <span class="kc">false</span><span class="p">)</span> <span class="c1">// => press cancel</span>
<span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="nx">tableRows</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">'a.btn.btn-danger'</span><span class="p">))</span>
<span class="c1">// check title, table rows, and updated job exists</span>
<span class="kr">await</span> <span class="nx">t</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">title</span><span class="p">.</span><span class="nx">innerText</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="s1">'All Jobs'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="nx">tableRows</span><span class="p">.</span><span class="nx">count</span><span class="p">).</span><span class="nx">eql</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span> <span class="c1">// why 3?</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Run the tests:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Node Jobs
✓ All Jobs
✓ New Job
✓ Update Job
✓ Delete Job
4 passed <span class="o">(</span>9s<span class="o">)</span>
</code></pre></div></div>
<p>Again, handle any edge cases on you own and clean up the code smell.</p>
<h2 id="browser-support">Browser Support</h2>
<p>Aside for Chrome, TestCafe <a href="http://devexpress.github.io/testcafe/documentation/using-testcafe/common-concepts/browsers/browser-support.html#officially-supported-browsers">supports</a> a number of browsers out-of-the-box. Further, if you don’t need to test browser-dependent functionality, then you can use a headless browser.</p>
<p>Start by installing the <a href="https://github.com/ryx/testcafe-browser-provider-nightmare">plugin</a>, which is powered by <a href="https://github.com/segmentio/nightmare">Nightmare</a>:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>npm install testcafe-browser-provider-nightmare@0.0.4 <span class="nt">--save-dev</span>
</code></pre></div></div>
<p>Update the <code class="highlighter-rouge">test</code> command in <em>package.json</em>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node_modules/testcafe/bin/testcafe.js nightmare tests/jobs.js --app 'npm start'"</span><span class="w">
</span></code></pre></div></div>
<p>Run the tests, and you should see:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Running tests <span class="k">in</span>:
- Electron 1.6.2 / Mac OS X 10.11.6
Node Jobs
✓ All Jobs
✓ New Job
✓ Update Job
✓ Delete Job
4 passed <span class="o">(</span>9s<span class="o">)</span>
</code></pre></div></div>
<p>There’s also a <a href="https://github.com/DevExpress/testcafe-browser-provider-saucelabs">plugin</a> for cross browser support powered by <a href="https://saucelabs.com/">SauceLabs</a>.</p>
<h2 id="continuous-integration">Continuous Integration</h2>
<p>Finally, let’s incorporate TestCafe into our Continuous Integration (CI) process with <a href="https://travis-ci.org/">Travis CI</a>.</p>
<blockquote>
<p><strong>NOTE:</strong> New to Travis? Review the <a href="https://docs.travis-ci.com/user/for-beginners">Travis CI for Complete Beginners</a> guide along with <a href="http://devexpress.github.io/testcafe/documentation/recipes/running-tests-in-firefox-and-chrome-using-travis-ci.html">Running Tests in Firefox and Chrome Using Travis CI</a>.</p>
</blockquote>
<p>After you enable Travis CI for the repository you are working with, add a <em>.travis.yml</em> file to the project root:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>language: node_js
node_js: "7"
dist: trusty
sudo: required
addons:
apt:
sources:
- google-chrome
packages:
- google-chrome-stable
before_script:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
- sleep 3
</code></pre></div></div>
<p>Here, we added the Node version along with some basic Chrome settings. Also, we have to use <a href="https://docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI">xvfb</a> to fake a GUI so that Chrome thinks it’s running in a graphical environment.</p>
<hr />
<p><br /></p>
<p>That’s it. Grab the final code from the <a href="https://github.com/mjhea0/testcafe-example">testcafe-example</a> repo. Comment below if you have questions.</p>
Michael Herman
Today we are going to dive into the world of functional web testing with TestCafe.
Token-Based Authentication with Angular
2017-01-05T00:00:00-06:00
2017-01-05T00:00:00-06:00
https://mherman.org/blog/token-based-authentication-with-angular
<p>In the <a href="http://mherman.org/blog/2016/10/28/token-based-authentication-with-node">Token-Based Authentication With Node</a> tutorial, we looked at how to add token-based authentication to a Node app using JSON Web Tokens (JWTs). This time, we’ll build out the client-side by showing how to add auth to Angular using JWTs.</p>
<div style="text-align:center;">
<img src="/assets/img/blog/angular-logo.png" style="max-width: 500px; border:0; box-shadow: none;" alt="angular logo" />
</div>
<p><br /></p>
<h2 class="no_toc" id="contents">Contents</h2>
<ul id="markdown-toc">
<li><a href="#objectives" id="markdown-toc-objectives">Objectives</a></li>
<li><a href="#review" id="markdown-toc-review">Review</a></li>
<li><a href="#project-setup" id="markdown-toc-project-setup">Project Setup</a></li>
<li><a href="#auth-component" id="markdown-toc-auth-component">Auth Component</a></li>
<li><a href="#service" id="markdown-toc-service">Service</a></li>
<li><a href="#server-side-setup" id="markdown-toc-server-side-setup">Server-side Setup</a></li>
<li><a href="#sanity-check-1" id="markdown-toc-sanity-check-1">Sanity Check</a></li>
<li><a href="#auth-login" id="markdown-toc-auth-login">Auth Login</a></li>
<li><a href="#auth-register" id="markdown-toc-auth-register">Auth Register</a></li>
<li><a href="#localstorage" id="markdown-toc-localstorage">LocalStorage</a></li>
<li><a href="#user-status" id="markdown-toc-user-status">User Status</a></li>
<li><a href="#route-restriction" id="markdown-toc-route-restriction">Route Restriction</a></li>
<li><a href="#whats-next" id="markdown-toc-whats-next">What’s Next?</a></li>
</ul>
<h2 id="objectives">Objectives</h2>
<p>By the end of this tutorial, you will be able to…</p>
<ol>
<li>Discuss the benefits of using JWTs versus sessions and cookies</li>
<li>Discuss the overall client/server authentication workflow</li>
<li>Implement user authentication using JWTs with Angular</li>
</ol>
<h2 id="review">Review</h2>
<p>Before beginning, review the <em>Introduction</em> from <a href="http://mherman.org/blog/2016/10/28/token-based-authentication-with-node">Token-Based Authentication With Node</a> so you have a solid understanding of what JWTs are and why you would want to use tokens over sessions for auth.</p>
<p>Make sure you can describe what’s happening on the server-side as well. Review the code from the <a href="https://github.com/mjhea0/node-token-auth">node-token-auth</a> repo, if necessary.</p>
<p>With that, here’s the full user auth process:</p>
<ol>
<li>Client logs in and the credentials are sent to the server</li>
<li>Server generates a token (if the credentials are correct)</li>
<li>Client receives and stores the token in local storage</li>
<li>Client then sends token to server on subsequent requests within the request header</li>
</ol>
<h2 id="project-setup">Project Setup</h2>
<p>Start by cloning the project structure:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/mjhea0/angular-token-auth <span class="nt">--branch</span> v1 <span class="nt">--single-branch</span> <span class="nt">-b</span> master
</code></pre></div></div>
<p>Install the dependencies, and then fire up the app by running <code class="highlighter-rouge">gulp</code> to make sure all is well. Navigate to http://localhost:8888 in your browser and you should see:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello World
sanity check
</code></pre></div></div>
<p>Kill the server when done, and then glance over the code within the project folder:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├── README.md
├── gulpfile.js
├── package.json
└── src
├── css
│ └── main.css
├── index.html
└── js
├── app.js
├── components
│ └── main
│ ├── main.controller.js
│ └── main.view.html
└── config.js
</code></pre></div></div>
<p>All of the client-side code lives in the “src” folder and the Angular app can be found in the “js” folder. Make sure you understand the app structure before moving on.</p>
<blockquote>
<p><strong>NOTE:</strong> This app uses Angular version <a href="https://code.angularjs.org/1.6.1/docs/api">1.6.1</a>.</p>
</blockquote>
<p>This is optional, but it’s a good idea to create a new Github repository and update the remote:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git remote set-url origin <newurl>
</code></pre></div></div>
<p>Now, let’s wire up a new component…</p>
<h2 id="auth-component">Auth Component</h2>
<p>First, add the dependency to the setter array within <em>app.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">angular</span>
<span class="p">.</span><span class="nx">module</span><span class="p">(</span><span class="s1">'tokenAuthApp'</span><span class="p">,</span> <span class="p">[</span>
<span class="s1">'ngRoute'</span><span class="p">,</span>
<span class="s1">'tokenAuthApp.config'</span><span class="p">,</span>
<span class="s1">'tokenAuthApp.components.main'</span><span class="p">,</span>
<span class="s1">'tokenAuthApp.components.auth'</span>
<span class="p">]);</span>
</code></pre></div></div>
<p>Create a new folder within “components” called “auth”, and then add the following two files to that folder…</p>
<p><em>auth.controller.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="s1">'use strict'</span><span class="p">;</span>
<span class="nx">angular</span>
<span class="p">.</span><span class="nx">module</span><span class="p">(</span><span class="s1">'tokenAuthApp.components.auth'</span><span class="p">,</span> <span class="p">[])</span>
<span class="p">.</span><span class="nx">controller</span><span class="p">(</span><span class="s1">'authLoginController'</span><span class="p">,</span> <span class="nx">authLoginController</span><span class="p">);</span>
<span class="nx">authLoginController</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[];</span>
<span class="kd">function</span> <span class="nx">authLoginController</span><span class="p">()</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="kd">const</span> <span class="nx">vm</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">test</span> <span class="o">=</span> <span class="s1">'just a test'</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">})();</span>
</code></pre></div></div>
<p><em>auth.login.view.html</em>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><h1></span>Login<span class="nt"></h1></span>
<span class="nt"><p></span>{{authLoginCtrl.test}}<span class="nt"></p></span>
</code></pre></div></div>
<p>Next, add the script tag to <em>index.html</em>, just before the closing body tag:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- auth component --></span>
<span class="nt"><script </span><span class="na">type=</span><span class="s">"text/javascript"</span> <span class="na">src=</span><span class="s">"js/components/auth/auth.controller.js"</span><span class="nt">></script></span>
</code></pre></div></div>
<p>Add a new route handler to the <em>config.js</em> file:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">appConfig</span><span class="p">(</span><span class="nx">$routeProvider</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">$routeProvider</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/main/main.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'mainController'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/login'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/auth/auth.login.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'authLoginController'</span><span class="p">,</span>
<span class="na">controllerAs</span><span class="p">:</span> <span class="s1">'authLoginCtrl'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">otherwise</span><span class="p">({</span>
<span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Run gulp, and then navigate to http://localhost:8888/#!/login. If all went well you should see the <code class="highlighter-rouge">just a test</code> text.</p>
<h2 id="service">Service</h2>
<p>Next, let’s create a global service to handle a user logging in, logging out, and signing up. Add a new file called <em>services.js</em> to the “js” directory:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="s1">'use strict'</span><span class="p">;</span>
<span class="nx">angular</span>
<span class="p">.</span><span class="nx">module</span><span class="p">(</span><span class="s1">'tokenAuthApp.services'</span><span class="p">,</span> <span class="p">[])</span>
<span class="p">.</span><span class="nx">service</span><span class="p">(</span><span class="s1">'authService'</span><span class="p">,</span> <span class="nx">authService</span><span class="p">);</span>
<span class="nx">authService</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[];</span>
<span class="kd">function</span> <span class="nx">authService</span><span class="p">()</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="k">this</span><span class="p">.</span><span class="nx">test</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="s1">'working'</span><span class="p">;</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">})();</span>
</code></pre></div></div>
<p>Make sure to add it to the dependencies in <em>app.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">angular</span>
<span class="p">.</span><span class="nx">module</span><span class="p">(</span><span class="s1">'tokenAuthApp'</span><span class="p">,</span> <span class="p">[</span>
<span class="s1">'ngRoute'</span><span class="p">,</span>
<span class="s1">'tokenAuthApp.config'</span><span class="p">,</span>
<span class="s1">'tokenAuthApp.components.main'</span><span class="p">,</span>
<span class="s1">'tokenAuthApp.components.auth'</span><span class="p">,</span>
<span class="s1">'tokenAuthApp.services'</span>
<span class="p">]);</span>
</code></pre></div></div>
<p>Add the script to the <em>index.html</em> file, below the config script:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><script </span><span class="na">type=</span><span class="s">"text/javascript"</span> <span class="na">src=</span><span class="s">"./js/services.js"</span><span class="nt">></script></span>
</code></pre></div></div>
<h3 id="sanity-check">Sanity Check</h3>
<p>Before adding code to <code class="highlighter-rouge">authService()</code>, let’s make sure the service itself is wired up correctly. To do that, within <em>auth.controller.js</em> inject the service and call the <code class="highlighter-rouge">test()</code> method:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">authLoginController</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'authService'</span><span class="p">];</span>
<span class="kd">function</span> <span class="nx">authLoginController</span><span class="p">(</span><span class="nx">authService</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="kd">const</span> <span class="nx">vm</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">test</span> <span class="o">=</span> <span class="s1">'just a test'</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">authService</span><span class="p">.</span><span class="nx">test</span><span class="p">());</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Run the server and then navigate to http://localhost:8888/#!/login. You should see <code class="highlighter-rouge">working</code> logged to the JS console.</p>
<h3 id="user-login">User Login</h3>
<p>To handle logging a user in, update the <code class="highlighter-rouge">authService()</code> like so:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">authService</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'$http'</span><span class="p">];</span>
<span class="kd">function</span> <span class="nx">authService</span><span class="p">(</span><span class="nx">$http</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="kd">const</span> <span class="nx">baseURL</span> <span class="o">=</span> <span class="s1">'http://localhost:3000/auth/'</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">login</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">$http</span><span class="p">({</span>
<span class="na">method</span><span class="p">:</span> <span class="s1">'POST'</span><span class="p">,</span>
<span class="na">url</span><span class="p">:</span> <span class="nx">baseURL</span> <span class="o">+</span> <span class="s1">'login'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="nx">user</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span><span class="s1">'Content-Type'</span><span class="p">:</span> <span class="s1">'application/json'</span><span class="p">}</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here, we are using the <code class="highlighter-rouge">$http</code> service to send an AJAX request to the <code class="highlighter-rouge">/user/login</code> endpoint. This returns a promise object.</p>
<p>Make sure to remove <code class="highlighter-rouge">console.log(authService.test());</code> from the controller.</p>
<h3 id="user-registration">User Registration</h3>
<p>Registering a user is similar to logging a user in:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="p">.</span><span class="nx">register</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">$http</span><span class="p">({</span>
<span class="na">method</span><span class="p">:</span> <span class="s1">'POST'</span><span class="p">,</span>
<span class="na">url</span><span class="p">:</span> <span class="nx">baseURL</span> <span class="o">+</span> <span class="s1">'register'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="nx">user</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span><span class="s1">'Content-Type'</span><span class="p">:</span> <span class="s1">'application/json'</span><span class="p">}</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>To test this we need to set up a back end…</p>
<h2 id="server-side-setup">Server-side Setup</h2>
<p>For the server-side, we’ll use the finished project from the previous blog post, <a href="http://mherman.org/blog/2016/10/28/token-based-authentication-with-node/">Token-Based Authentication With Node</a>. You can view the code from the <a href="https://github.com/mjhea0/node-token-auth">node-token-auth</a> repository.</p>
<blockquote>
<p><strong>NOTE:</strong> Feel free to use your own server, just make sure to update the <code class="highlighter-rouge">baseURL</code> in the service.</p>
</blockquote>
<p>Clone the project structure in a new terminal window:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git clone https://github.com/mjhea0/node-token-auth <span class="nt">--branch</span> v2 <span class="nt">--single-branch</span> <span class="nt">-b</span> master
</code></pre></div></div>
<p>Follow the directions in the <a href="https://github.com/mjhea0/node-token-auth/blob/v2/README.md">README</a> to set up the project. Once done, run the server with <code class="highlighter-rouge">npm start</code>, which will listen on port 3000.</p>
<h2 id="sanity-check-1">Sanity Check</h2>
<p>To test, update <code class="highlighter-rouge">authLoginController()</code> like so:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">authLoginController</span><span class="p">(</span><span class="nx">authService</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="kd">const</span> <span class="nx">vm</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">test</span> <span class="o">=</span> <span class="s1">'just a test'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">sampleUser</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">username</span><span class="p">:</span> <span class="s1">'michael'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="s1">'herman'</span>
<span class="p">};</span>
<span class="nx">authService</span><span class="p">.</span><span class="nx">register</span><span class="p">(</span><span class="nx">sampleUser</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">authService</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="nx">sampleUser</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In the browser, you should see the following errors in the console at http://localhost:8888/#!/login:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>XMLHttpRequest cannot load http://localhost:3000/auth/register. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8888' is therefore not allowed access.
</code></pre></div></div>
<p>This is a <a href="http://enable-cors.org/">CORS issue</a>. To fix, we need to <a href="http://enable-cors.org/server_expressjs.html">update</a> the server. Add the following code to <em>src/server/config/main-config.js</em>, just above <code class="highlighter-rouge">app.use(cookieParser());</code>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// *** cross domain requests *** //</span>
<span class="kd">const</span> <span class="nx">allowCrossDomain</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">,</span> <span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">header</span><span class="p">(</span><span class="s1">'Access-Control-Allow-Origin'</span><span class="p">,</span> <span class="s1">'*'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">header</span><span class="p">(</span><span class="s1">'Access-Control-Allow-Methods'</span><span class="p">,</span> <span class="s1">'GET, POST, PUT, DELETE'</span><span class="p">);</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">header</span><span class="p">(</span><span class="s1">'Access-Control-Allow-Headers'</span><span class="p">,</span> <span class="s1">'Content-Type, Authorization'</span><span class="p">);</span>
<span class="nx">next</span><span class="p">();</span>
<span class="p">};</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">allowCrossDomain</span><span class="p">);</span>
</code></pre></div></div>
<p>Refresh http://localhost:8888/#!/login in the browser and you should see a success in the console with the token:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="s2">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="p">,</span><span class="w">
</span><span class="s2">"token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0ODQ2NzY4MjEsImlhdCI6MTQ4MzQ2NzIyMSwic3ViIjoyfQ.hMcrXz-63iD4jX-ves3cZMznSS3UhZD4NCPry2zLkHo"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h2 id="auth-login">Auth Login</h2>
<p>Update <em>auth.login.view.html</em>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"col-md-4"</span><span class="nt">></span>
<span class="nt"><h1></span>Login<span class="nt"></h1></span>
<span class="nt"><hr><br></span>
<span class="nt"><form</span> <span class="na">ng-submit=</span><span class="s">"authLoginCtrl.onLogin()"</span> <span class="na">novalidate</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"username"</span><span class="nt">></span>Username<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">id=</span><span class="s">"username"</span> <span class="na">placeholder=</span><span class="s">"enter username"</span> <span class="na">ng-model=</span><span class="s">"authLoginCtrl.user.username"</span> <span class="na">required</span><span class="nt">></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"passwowrd"</span><span class="nt">></span>Password<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"password"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">id=</span><span class="s">"passwowrd"</span> <span class="na">placeholder=</span><span class="s">"enter password"</span> <span class="na">ng-model=</span><span class="s">"authLoginCtrl.user.password"</span> <span class="na">required</span><span class="nt">></span>
<span class="nt"></div></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">"submit"</span> <span class="na">class=</span><span class="s">"btn btn-default"</span><span class="nt">></span>Submit<span class="nt"></button></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>Take note of the form. We used the <code class="highlighter-rouge">ng-model</code> directive on each of the form inputs to capture those values in the controller. Also, when the form is submitted, the <code class="highlighter-rouge">ng-submit</code> directive handles the event by firing the <code class="highlighter-rouge">onLogin()</code> function.</p>
<p>Now, let’s update the controller:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">authLoginController</span><span class="p">(</span><span class="nx">authService</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="kd">const</span> <span class="nx">vm</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">user</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">onLogin</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">authService</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="nx">vm</span><span class="p">.</span><span class="nx">user</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>
<p>So, when the form is submitted, we capture the username and password and pass them to the <code class="highlighter-rouge">login()</code> method on the service.</p>
<p>Test this out.</p>
<h2 id="auth-register">Auth Register</h2>
<p>Just like the login, we need to add a view and controller for registering a user. Start by adding the view, <em>auth.register.view.html</em>, to the “auth” folder:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"col-md-4"</span><span class="nt">></span>
<span class="nt"><h1></span>Register<span class="nt"></h1></span>
<span class="nt"><hr><br></span>
<span class="nt"><form</span> <span class="na">ng-submit=</span><span class="s">"authRegisterCtrl.onRegister()"</span> <span class="na">novalidate</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"username"</span><span class="nt">></span>Username<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">id=</span><span class="s">"username"</span> <span class="na">placeholder=</span><span class="s">"enter username"</span> <span class="na">ng-model=</span><span class="s">"authRegisterCtrl.user.username"</span> <span class="na">required</span><span class="nt">></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"form-group"</span><span class="nt">></span>
<span class="nt"><label</span> <span class="na">for=</span><span class="s">"passwowrd"</span><span class="nt">></span>Password<span class="nt"></label></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"password"</span> <span class="na">class=</span><span class="s">"form-control"</span> <span class="na">id=</span><span class="s">"passwowrd"</span> <span class="na">placeholder=</span><span class="s">"enter password"</span> <span class="na">ng-model=</span><span class="s">"authRegisterCtrl.user.password"</span> <span class="na">required</span><span class="nt">></span>
<span class="nt"></div></span>
<span class="nt"><button</span> <span class="na">type=</span><span class="s">"submit"</span> <span class="na">class=</span><span class="s">"btn btn-default"</span><span class="nt">></span>Submit<span class="nt"></button></span>
<span class="nt"></form></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>Add a new controller to <em>auth.controller.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">authRegisterController</span><span class="p">(</span><span class="nx">authService</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="kd">const</span> <span class="nx">vm</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">user</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">onRegister</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">authService</span><span class="p">.</span><span class="nx">register</span><span class="p">(</span><span class="nx">vm</span><span class="p">.</span><span class="nx">user</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Don’t forget:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">angular</span>
<span class="p">.</span><span class="nx">module</span><span class="p">(</span><span class="s1">'tokenAuthApp.components.auth'</span><span class="p">,</span> <span class="p">[])</span>
<span class="p">.</span><span class="nx">controller</span><span class="p">(</span><span class="s1">'authLoginController'</span><span class="p">,</span> <span class="nx">authLoginController</span><span class="p">)</span>
<span class="p">.</span><span class="nx">controller</span><span class="p">(</span><span class="s1">'authRegisterController'</span><span class="p">,</span> <span class="nx">authRegisterController</span><span class="p">);</span>
<span class="nx">authLoginController</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'authService'</span><span class="p">];</span>
<span class="nx">authRegisterController</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'authService'</span><span class="p">];</span>
</code></pre></div></div>
<p>Add a new route handler to the <em>config.js</em> file:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">appConfig</span><span class="p">(</span><span class="nx">$routeProvider</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">$routeProvider</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/main/main.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'mainController'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/login'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/auth/auth.login.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'authLoginController'</span><span class="p">,</span>
<span class="na">controllerAs</span><span class="p">:</span> <span class="s1">'authLoginCtrl'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/register'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/auth/auth.register.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'authRegisterController'</span><span class="p">,</span>
<span class="na">controllerAs</span><span class="p">:</span> <span class="s1">'authRegisterCtrl'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">otherwise</span><span class="p">({</span>
<span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Test it out by registering a new user!</p>
<h2 id="localstorage">LocalStorage</h2>
<p>Next, let’s add the token to localStorage for persistence by replacing the <code class="highlighter-rouge">console.log(user.data);</code> with <code class="highlighter-rouge">localStorage.setItem('token', user.data.token);</code>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">authLoginController</span><span class="p">(</span><span class="nx">authService</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="kd">const</span> <span class="nx">vm</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">user</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">onLogin</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">authService</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="nx">vm</span><span class="p">.</span><span class="nx">user</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">token</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">authRegisterController</span><span class="p">(</span><span class="nx">authService</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="kd">const</span> <span class="nx">vm</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">user</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">onRegister</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">authService</span><span class="p">.</span><span class="nx">register</span><span class="p">(</span><span class="nx">vm</span><span class="p">.</span><span class="nx">user</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">token</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>
<p>As long as that token is present, the user can be considered logged in. And, when a user needs to make an AJAX request, that token can be used.</p>
<blockquote>
<p><strong>NOTE</strong>: Besides the token, you could also add the user id and username. You would just need to update the server-side to send back that info.</p>
</blockquote>
<p>Test this out. Ensure that the token is present in localStorage.</p>
<h2 id="user-status">User Status</h2>
<p>To test out login persistence, we can add a new view that verifies that the user is logged in and that the token is valid.</p>
<p>Add the following method to <code class="highlighter-rouge">authService()</code>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="p">.</span><span class="nx">ensureAuthenticated</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">$http</span><span class="p">({</span>
<span class="na">method</span><span class="p">:</span> <span class="s1">'GET'</span><span class="p">,</span>
<span class="na">url</span><span class="p">:</span> <span class="nx">baseURL</span> <span class="o">+</span> <span class="s1">'user'</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'Content-Type'</span><span class="p">:</span> <span class="s1">'application/json'</span><span class="p">,</span>
<span class="na">Authorization</span><span class="p">:</span> <span class="s1">'Bearer '</span> <span class="o">+</span> <span class="nx">token</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Take note of <code class="highlighter-rouge">Authorization: 'Bearer ' + token</code>. This is called a <a href="http://security.stackexchange.com/questions/108662/why-is-bearer-required-before-the-token-in-authorization-header-in-a-http-re">Bearer schema</a>, which is sent along with the request. On the server, we are simply checking for the <code class="highlighter-rouge">Authorization</code> header, and then whether the token is valid. Can you find this code on the server-side?</p>
<p>Then add a new file called <em>auth.status.view.html</em> to the “auth” folder:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"col-md-4"</span><span class="nt">></span>
<span class="nt"><h1></span>User Status<span class="nt"></h1></span>
<span class="nt"><hr><br></span>
<span class="nt"><p></span>Logged In? {{ authStatusCtrl.isLoggedIn }}<span class="nt"></p></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>Add a new controller:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">authStatusController</span><span class="p">(</span><span class="nx">authService</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="kd">const</span> <span class="nx">vm</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">isLoggedIn</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">authService</span><span class="p">.</span><span class="nx">ensureAuthenticated</span><span class="p">(</span><span class="nx">token</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">'success'</span><span class="p">);</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">isLoggedIn</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">angular</span>
<span class="p">.</span><span class="nx">module</span><span class="p">(</span><span class="s1">'tokenAuthApp.components.auth'</span><span class="p">,</span> <span class="p">[])</span>
<span class="p">.</span><span class="nx">controller</span><span class="p">(</span><span class="s1">'authLoginController'</span><span class="p">,</span> <span class="nx">authLoginController</span><span class="p">)</span>
<span class="p">.</span><span class="nx">controller</span><span class="p">(</span><span class="s1">'authRegisterController'</span><span class="p">,</span> <span class="nx">authRegisterController</span><span class="p">)</span>
<span class="p">.</span><span class="nx">controller</span><span class="p">(</span><span class="s1">'authStatusController'</span><span class="p">,</span> <span class="nx">authStatusController</span><span class="p">);</span>
<span class="nx">authLoginController</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'authService'</span><span class="p">];</span>
<span class="nx">authRegisterController</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'authService'</span><span class="p">];</span>
<span class="nx">authStatusController</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'authService'</span><span class="p">];</span>
</code></pre></div></div>
<p>Finally, update <em>config.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">appConfig</span><span class="p">(</span><span class="nx">$routeProvider</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">$routeProvider</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/main/main.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'mainController'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/login'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/auth/auth.login.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'authLoginController'</span><span class="p">,</span>
<span class="na">controllerAs</span><span class="p">:</span> <span class="s1">'authLoginCtrl'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/register'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/auth/auth.register.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'authRegisterController'</span><span class="p">,</span>
<span class="na">controllerAs</span><span class="p">:</span> <span class="s1">'authRegisterCtrl'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/status'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/auth/auth.status.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'authStatusController'</span><span class="p">,</span>
<span class="na">controllerAs</span><span class="p">:</span> <span class="s1">'authStatusCtrl'</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">otherwise</span><span class="p">({</span>
<span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Test this out at http://localhost:8888/#!/status:</p>
<ul>
<li>If there is a token in localStorage, you should see - <code class="highlighter-rouge">Logged In? true</code></li>
<li>Otherwise, you should see <code class="highlighter-rouge">Logged In? false</code></li>
</ul>
<p>Finally, let’s redirect to the status page after a user successfully registers or logs in. Update the controllers like so:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">authLoginController</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'$location'</span><span class="p">,</span> <span class="s1">'authService'</span><span class="p">];</span>
<span class="nx">authRegisterController</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'$location'</span><span class="p">,</span> <span class="s1">'authService'</span><span class="p">];</span>
<span class="nx">authStatusController</span><span class="p">.</span><span class="nx">$inject</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'authService'</span><span class="p">];</span>
<span class="kd">function</span> <span class="nx">authLoginController</span><span class="p">(</span><span class="nx">$location</span><span class="p">,</span> <span class="nx">authService</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="kd">const</span> <span class="nx">vm</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">user</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">onLogin</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">authService</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="nx">vm</span><span class="p">.</span><span class="nx">user</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">token</span><span class="p">);</span>
<span class="nx">$location</span><span class="p">.</span><span class="nx">path</span><span class="p">(</span><span class="s1">'/status'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">authRegisterController</span><span class="p">(</span><span class="nx">$location</span><span class="p">,</span> <span class="nx">authService</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/*jshint validthis: true */</span>
<span class="kd">const</span> <span class="nx">vm</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">user</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nx">vm</span><span class="p">.</span><span class="nx">onRegister</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">authService</span><span class="p">.</span><span class="nx">register</span><span class="p">(</span><span class="nx">vm</span><span class="p">.</span><span class="nx">user</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">token</span><span class="p">);</span>
<span class="nx">$location</span><span class="p">.</span><span class="nx">path</span><span class="p">(</span><span class="s1">'/status'</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Test it out!</p>
<h2 id="route-restriction">Route Restriction</h2>
<p>Right now, all routes are open; so, regardless of whether a user is logged in or not they, they can access each route. Certain routes should be restricted if a user is not logged in, while other routes should be restricted if a user is logged in:</p>
<ol>
<li><code class="highlighter-rouge">/</code> - no restrictions</li>
<li><code class="highlighter-rouge">/login</code> - restricted when logged in</li>
<li><code class="highlighter-rouge">/register</code> - restricted when logged in</li>
<li><code class="highlighter-rouge">status</code> - restricted when not logged in</li>
</ol>
<p>To achieve this, add the following property to each route, replacing <code class="highlighter-rouge">false</code> with <code class="highlighter-rouge">true</code> for routes that you want to restrict:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">restrictions</span><span class="p">:</span> <span class="p">{</span>
<span class="nl">ensureAuthenticated</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nx">loginRedirect</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">}</span>
</code></pre></div></div>
<p>For example:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">appConfig</span><span class="p">(</span><span class="nx">$routeProvider</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">$routeProvider</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/main/main.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'mainController'</span><span class="p">,</span>
<span class="na">restrictions</span><span class="p">:</span> <span class="p">{</span>
<span class="na">ensureAuthenticated</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="na">loginRedirect</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/login'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/auth/auth.login.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'authLoginController'</span><span class="p">,</span>
<span class="na">controllerAs</span><span class="p">:</span> <span class="s1">'authLoginCtrl'</span><span class="p">,</span>
<span class="na">restrictions</span><span class="p">:</span> <span class="p">{</span>
<span class="na">ensureAuthenticated</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="na">loginRedirect</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/register'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/auth/auth.register.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'authRegisterController'</span><span class="p">,</span>
<span class="na">controllerAs</span><span class="p">:</span> <span class="s1">'authRegisterCtrl'</span><span class="p">,</span>
<span class="na">restrictions</span><span class="p">:</span> <span class="p">{</span>
<span class="na">ensureAuthenticated</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="na">loginRedirect</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="s1">'/status'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="s1">'js/components/auth/auth.status.view.html'</span><span class="p">,</span>
<span class="na">controller</span><span class="p">:</span> <span class="s1">'authStatusController'</span><span class="p">,</span>
<span class="na">controllerAs</span><span class="p">:</span> <span class="s1">'authStatusCtrl'</span><span class="p">,</span>
<span class="na">restrictions</span><span class="p">:</span> <span class="p">{</span>
<span class="na">ensureAuthenticated</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">loginRedirect</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">.</span><span class="nx">otherwise</span><span class="p">({</span>
<span class="na">redirectTo</span><span class="p">:</span> <span class="s1">'/'</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Next, add the following function below the route handlers in <em>config.js</em>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">routeStart</span><span class="p">(</span><span class="nx">$rootScope</span><span class="p">,</span> <span class="nx">$location</span><span class="p">,</span> <span class="nx">$route</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">$rootScope</span><span class="p">.</span><span class="nx">$on</span><span class="p">(</span><span class="s1">'$routeChangeStart'</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">,</span> <span class="nx">next</span><span class="p">,</span> <span class="nx">current</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">next</span><span class="p">.</span><span class="nx">restrictions</span><span class="p">.</span><span class="nx">ensureAuthenticated</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">$location</span><span class="p">.</span><span class="nx">path</span><span class="p">(</span><span class="s1">'/login'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">next</span><span class="p">.</span><span class="nx">restrictions</span><span class="p">.</span><span class="nx">loginRedirect</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">'token'</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">$location</span><span class="p">.</span><span class="nx">path</span><span class="p">(</span><span class="s1">'/status'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The <code class="highlighter-rouge">$routeChangeStart</code> event fires before the actual route change occurs. So, whenever a route is accessed, we check the <code class="highlighter-rouge">restrictions</code> property:</p>
<ol>
<li>If <code class="highlighter-rouge">ensureAuthenticated</code> is true and there is no token present, then we redirect them to the login page</li>
<li>If <code class="highlighter-rouge">loginRedirect</code> is true and there’s a token present, then we redirect them to the status page</li>
</ol>
<p>Simple, right?</p>
<p>Update:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">angular</span>
<span class="p">.</span><span class="nx">module</span><span class="p">(</span><span class="s1">'tokenAuthApp.config'</span><span class="p">,</span> <span class="p">[])</span>
<span class="p">.</span><span class="nx">config</span><span class="p">(</span><span class="nx">appConfig</span><span class="p">)</span>
<span class="p">.</span><span class="nx">run</span><span class="p">(</span><span class="nx">routeStart</span><span class="p">);</span>
</code></pre></div></div>
<p>Then test one last time.</p>
<h2 id="whats-next">What’s Next?</h2>
<p>You’ve reached the end. Now what?</p>
<ol>
<li>You should handle those errors in each <code class="highlighter-rouge">.catch</code>.</li>
<li>Check out <a href="https://github.com/sahat/satellizer">Satellizer</a>. It’s a nice token-based auth module for Angular. You can find sample code in the following repos - <a href="https://github.com/mjhea0/mean-token-auth">mean-token-auth</a> and <a href="https://github.com/mjhea0/mean-social-token-auth">mean-social-token-auth</a>.</li>
<li>Try using this app with a different back-end. Since this app is just the client, you can literally use any language/framework to write a RESTful API in. Want to try Python and Flask? Check out <a href="https://realpython.com/blog/python/token-based-authentication-with-flask/">Token-Based Authentication With Flask</a>.</li>
</ol>
<p>Grab the final code from the <a href="https://github.com/mjhea0/angular-token-auth">angular-token-auth</a> repo. Comment below. Cheers!</p>
Michael Herman
In the Token-Based Authentication With Node tutorial, we looked at how to add token-based authentication to a Node app using JSON Web Tokens (JWTs). This time, we’ll build out the client-side by showing how to add auth to Angular using JWTs.