avatarLukas Wimhofer

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

22723

Abstract

r</span>: e.<span class="hljs-property">message</span> }); } };</pre></div><p id="d1e8">When we now create a new user, the response will look like this:</p><div id="e667"><pre><span class="hljs-punctuation">{</span> <span class="hljs-attr">"user"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"email"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"[email protected]"</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">"name"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"user"</span> <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">}</span></pre></div><h1 id="093a">Authentication</h1><p id="0fe4">At the moment, we can call all of our API endpoints without authentication. However, we want to secure some of the endpoints so that you can only call the API when you are authenticated.</p><p id="b5d6">In an Express application, authentication typically involves the following steps:</p><ol><li>User provides their credentials (e.g. username and password) to the server via a login form or API endpoint.</li><li>The server verifies the user’s credentials, usually by checking them against a database of user accounts.</li><li>If the credentials are valid, the server generates an authentication token (e.g. a JSON Web Token or JWT) and sends it to the client.</li><li>The client stores the token (e.g. in a cookie or in local storage) and sends it with each subsequent request to the server.</li><li>The server checks the validity of the token on each request and grants access to protected resources only if the token is valid.</li></ol><p id="65c6">First we need to edit our user model so that we can authenticate users by using a password.</p><p id="76fc">Let’s edit our <code>schema.primsa</code> file:</p><div id="16b1"><pre>model <span class="hljs-title class_">User</span> { id <span class="hljs-title class_">String</span> <span class="hljs-meta">@id</span> <span class="hljs-meta">@default</span>(<span class="hljs-title function_">auto</span>()) <span class="hljs-meta">@map</span>(<span class="hljs-string">"_id"</span>) <span class="hljs-meta">@db</span>.<span class="hljs-property">ObjectId</span> email <span class="hljs-title class_">String</span> <span class="hljs-meta">@unique</span> name <span class="hljs-title class_">String</span> password <span class="hljs-title class_">String</span> }</pre></div><p id="f4f0">Now use this command to generate the new client:</p><div id="5107"><pre>npx prisma generate</pre></div><blockquote id="7b99"><p>Note that if the type User is not updated in other files after importing, close and open the editor again.</p></blockquote><p id="572a">Let’s also add the password to our Dto:</p><div id="f68a"><pre> <span class="hljs-meta">@IsString</span>() <span class="hljs-meta">@IsNotEmpty</span>() <span class="hljs-attr">password</span>: <span class="hljs-built_in">string</span>;</pre></div><p id="c488">Next we create a new interface called <code>auth.interface.ts</code> where we define the structures that we need for our auth service:</p><div id="324b"><pre><span class="hljs-keyword">import</span> { <span class="hljs-title class_">Request</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">User</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@prisma/client'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> <span class="hljs-title class_">DataStoredInToken</span> { <span class="hljs-attr">id</span>: <span class="hljs-built_in">string</span>; }

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> <span class="hljs-title class_">TokenData</span> { <span class="hljs-attr">token</span>: <span class="hljs-built_in">string</span>; <span class="hljs-attr">expiresIn</span>: <span class="hljs-built_in">number</span>; }

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> <span class="hljs-title class_">RequestWithUser</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Request</span> { <span class="hljs-attr">user</span>: <span class="hljs-title class_">User</span>; }</pre></div><blockquote id="f0be"><p>The reason you need this interface is that you want to attach additional properties to the standard <code>Request</code> object in Express.js. In this case, you want to attach a <code>user</code> property to the <code>Request</code> object that represents the authenticated user making the request.</p></blockquote><blockquote id="9aaf"><p>The <code>RequestWithUser</code> interface extends the <code>Request</code> interface from the <code>express</code> module, and adds a <code>user</code> property of type <code>User</code> from the Prisma client. This allows you to access the <code>user</code> property in subsequent middleware or route handlers, without having to typecast the <code>Request</code> object every time.</p></blockquote><blockquote id="9992"><p>The <code>DataStoredInToken</code> and <code>TokenData</code> interfaces are used to define the structure of the JSON Web Token that is generated and returned to the client during authentication.</p></blockquote><blockquote id="883d"><p><code>DataStoredInToken</code> defines the structure of the data that will be stored in the token, which in this case is just the <code>id</code> of the user.</p></blockquote><blockquote id="8b0e"><p><code>TokenData</code> defines the structure of the JSON Web Token itself, which consists of a <code>token</code> property containing the actual token string, and an <code>expiresIn</code> property indicating the expiration time of the token in seconds. These interfaces are used by the <code>createToken</code> method in your <code>AuthService</code> to generate and return the JWT token.</p></blockquote><p id="da90">Let’s also define our a new class called <code>HttpException</code> . For this we create a new directory called <code>exceptions</code> and a new file called <code>HttpException.ts</code> with the following code:</p><div id="4535"><pre><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">HttpException</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">Error</span> { <span class="hljs-keyword">public</span> <span class="hljs-attr">status</span>: <span class="hljs-built_in">number</span>; <span class="hljs-keyword">public</span> <span class="hljs-attr">message</span>: <span class="hljs-built_in">string</span>;

<span class="hljs-title function_">constructor</span>(<span class="hljs-params">status: <span class="hljs-built_in">number</span>, message: <span class="hljs-built_in">string</span></span>) { <span class="hljs-variable language_">super</span>(message); <span class="hljs-variable language_">this</span>.<span class="hljs-property">status</span> = status; <span class="hljs-variable language_">this</span>.<span class="hljs-property">message</span> = message; } }</pre></div><blockquote id="a7e2"><p>The <code>HttpException</code> class allows you to define a consistent structure for your errors, by including a <code>status</code> property that represents the HTTP status code to send in the response, and a <code>message</code> property that represents the error message to send. By creating custom errors that conform to this structure, you can ensure that your error-handling middleware can easily handle and respond to errors in a consistent and predictable way.</p></blockquote><p id="071e">Before we create the auth service, we want to install the libraries that we need for authenticating users with JWT:</p><div id="f192"><pre>npm <span class="hljs-selector-tag">i</span> jsonwebtoken bcrypt cookie-parser</pre></div><blockquote id="09a6"><p><code>jsonwebtoken</code>: This package is used for generating and verifying JSON Web Tokens (JWT), which are a standard for securely transmitting information between parties. You can use JWT to implement stateless authentication in your application, where the client sends the JWT token with each request to access protected resources.</p></blockquote><blockquote id="6b5a"><p><code>bcrypt</code>: This package is used for hashing and verifying passwords, and is a popular way to securely store passwords in databases. When a user creates an account or resets their password, you can use bcrypt to hash the password and store the hash in the database. When the user logs in, you can use bcrypt to verify the password against the stored hash.</p></blockquote><blockquote id="8351"><p><code>cookie-parser</code>: This package is used for parsing cookies in the request headers, and makes it easier to work with cookies in your application. You can use cookies to implement server-side sessions and maintain the user's authentication state across requests.</p></blockquote><p id="7c0f">We can go ahead and add the cookie parser to our <code>index.ts</code> file:</p><div id="ee84"><pre><span class="hljs-keyword">import</span> cookieParser <span class="hljs-keyword">from</span> <span class="hljs-string">'cookie-parser'</span>;</pre></div><div id="d67f"><pre><span class="hljs-comment">// App Configuration</span>

<span class="hljs-keyword">const</span> <span class="hljs-variable constant_">app</span> = <span class="hljs-title function_ invoke__">express</span>();

app.<span class="hljs-keyword">use</span>(<span class="hljs-title function_ invoke__">helmet</span>()); app.<span class="hljs-keyword">use</span>(<span class="hljs-title function_ invoke__">cors</span>()); app.<span class="hljs-keyword">use</span>(express.<span class="hljs-title function_ invoke__">json</span>()); app.<span class="hljs-keyword">use</span>(<span class="hljs-title function_ invoke__">cookieParser</span>()); </pre></div><p id="cca6">We will also need a SECRET_KEY env variable so go ahead and add it to the <code>.env</code> file:</p><div id="a018"><pre><span class="hljs-attr">SECRET_KEY</span>=yoursecret</pre></div><p id="946d">Also make sure to add to to our validation in <code>validate.Env.ts</code> :</p><div id="607e"><pre> SECRET_KEY: <span class="hljs-built_in">str</span>(),</pre></div><p id="6771">Now let’s create our <code>auth.service.ts</code> file and define our services:</p><div id="41cc"><pre><span class="hljs-keyword">import</span> { compare, hash } <span class="hljs-keyword">from</span> <span class="hljs-string">'bcrypt'</span>; <span class="hljs-keyword">import</span> { sign } <span class="hljs-keyword">from</span> <span class="hljs-string">'jsonwebtoken'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">PrismaClient</span>, <span class="hljs-title class_">User</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@prisma/client'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">CreateUserDto</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@dtos/users.dto'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">DataStoredInToken</span>, <span class="hljs-title class_">TokenData</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@interfaces/auth.interface'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">HttpException</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@exceptions/HttpException'</span>; <span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;

dotenv.<span class="hljs-title function_">config</span>();

<span class="hljs-keyword">const</span> <span class="hljs-variable constant_">SECRET_KEY</span> = process.<span class="hljs-property">env</span>.<span class="hljs-property">SECRET_KEY</span>;

<span class="hljs-keyword">class</span> <span class="hljs-title class_">AuthService</span> { <span class="hljs-keyword">public</span> users = <span class="hljs-keyword">new</span> <span class="hljs-title class_">PrismaClient</span>().<span class="hljs-property">user</span>;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> <span class="hljs-title function_">signup</span>(<span class="hljs-attr">data</span>: <span class="hljs-title class_">CreateUserDto</span>): <span class="hljs-title class_">Promise</span><<span class="hljs-title class_">User</span>> { <span class="hljs-keyword">const</span> <span class="hljs-attr">findUser</span>: <span class="hljs-title class_">User</span> = <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">users</span>.<span class="hljs-title function_">findUnique</span>({ <span class="hljs-attr">where</span>: { <span class="hljs-attr">email</span>: data.<span class="hljs-property">email</span> } }); <span class="hljs-keyword">if</span> (findUser) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>(<span class="hljs-number">409</span>, <span class="hljs-string">This email <span class="hljs-subst">${data.email}</span> already exists</span>);

<span class="hljs-keyword">const</span> hashedPassword = <span class="hljs-keyword">await</span> <span class="hljs-title function_">hash</span>(data.<span class="hljs-property">password</span>, <span class="hljs-number">10</span>);
<span class="hljs-keyword">const</span> <span class="hljs-attr">createUserData</span>: <span class="hljs-title class_">Promise</span>&lt;<span class="hljs-title class_">User</span>&gt; = <span class="hljs-variable language_">this</span>.<span class="hljs-property">users</span>.<span class="hljs-title function_">create</span>({ <span class="hljs-attr">data</span>: { ...data, <span class="hljs-attr">password</span>: hashedPassword } });

<span class="hljs-keyword">return</span> createUserData;

}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> <span class="hljs-title function_">login</span>(<span class="hljs-attr">data</span>: <span class="hljs-title class_">CreateUserDto</span>): <span class="hljs-title class_">Promise</span><{ <span class="hljs-attr">cookie</span>: <span class="hljs-built_in">string</span>; <span class="hljs-attr">findUser</span>: <span class="hljs-title class_">User</span> }> { <span class="hljs-keyword">const</span> <span class="hljs-attr">findUser</span>: <span class="hljs-title class_">User</span> = <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">users</span>.<span class="hljs-title function_">findUnique</span>({ <span class="hljs-attr">where</span>: { <span class="hljs-attr">email</span>: data.<span class="hljs-property">email</span> } }); <span class="hljs-keyword">if</span> (!findUser) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>(<span class="hljs-number">409</span>, <span class="hljs-string">This email <span class="hljs-subst">${data.email}</span> was not found</span>);

<span class="hljs-keyword">const</span> <span class="hljs-attr">isPasswordMatching</span>: <span class="hljs-built_in">boolean</span> = <span class="hljs-keyword">await</span> <span class="hljs-title function_">compare</span>(data.<span class="hljs-property">password</span>, findUser.<span class="hljs-property">password</span>);
<span class="hljs-keyword">if</span> (!isPasswordMatching) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>(<span class="hljs-number">409</span>, <span class="hljs-string">'Password is not matching'</span>);

<span class="hljs-keyword">const</span> tokenData = <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">createToken</span>(findUser);
<span class="hljs-keyword">const</span> cookie = <span class="hljs-variable language_">this</span>.<span class="hljs-title function_">createCookie</span>(tokenData);

<span class="hljs-keyword">return</span> { cookie, findUser };

}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> <span class="hljs-title function_">logout</span>(<span class="hljs-attr">data</span>: <span class="hljs-title class_">User</span>): <span class="hljs-title class_">Promise</span><<span class="hljs-title class_">User</span>> { <span class="hljs-keyword">const</span> <span class="hljs-attr">findUser</span>: <span class="hljs-title class_">User</span> = <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">users</span>.<span class="hljs-title function_">findFirst</span>({ <span class="hljs-attr">where</span>: { <span class="hljs-attr">email</span>: data.<span class="hljs-property">email</span>, <span class="hljs-attr">password</span>: data.<span class="hljs-property">password</span> } }); <span class="hljs-keyword">if</span> (!findUser) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>(<span class="hljs-number">409</span>, <span class="hljs-string">"User doesn't exist"</span>);

<span class="hljs-keyword">return</span> findUser;

}

<span class="hljs-keyword">public</span> <span class="hljs-title function_">createToken</span>(<span class="hljs-attr">user</span>: <span class="hljs-title class_">User</span>): <span class="hljs-title class_">TokenData</span> { <span class="hljs-keyword">const</span> <span class="hljs-attr">dataStoredInToken</span>: <span class="hljs-title class_">DataStoredInToken</span> = { <span class="hljs-attr">id</span>: user.<span class="hljs-property">id</span> }; <span class="hljs-keyword">const</span> <span class="hljs-attr">secretKey</span>: <span class="hljs-built_in">string</span> = <span class="hljs-variable constant_">SECRET_KEY</span>; <span class="hljs-keyword">const</span> <span class="hljs-attr">expiresIn</span>: <span class="hljs-built_in">number</span> = <span class="hljs-number">60</span> * <span class="hljs-number">60</span>;

<span class="hljs-keyword">return</span> { expiresIn, <span class="hljs-attr">token</span>: <span class="hljs-title function_">sign</span>(dataStoredInToken, secretKey, { expiresIn }) };

}

<span class="hljs-keyword">public</span> <span class="hljs-title function_">createCookie</span>(<span class="hljs-attr">tokenData</span>: <span class="hljs-title class_">TokenData</span>): <span class="hljs-built_in">string</span> { <span class="hljs-keyword">return</span> <span class="hljs-string">Authorization=<span class="hljs-subst">${tokenData.token}</span>; HttpOnly; Max-Age=<span class="hljs-subst">${tokenData.expiresIn}</span>;</span>; } }

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title class_">AuthService</span>;</pre></div><p id="5010">Now we can create the <code>auth.controller.ts</code> file:</p><div id="68bd"><pre><span class="hljs-keyword">import</span> { <span class="hljs-title class_">NextFunction</span>, <span class="hljs-title class_">Request</span>, <span class="hljs-title class_">Response</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">User</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@prisma/client'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">CreateUserDto</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@dtos/users.dto'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">RequestWithUser</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@interfaces/auth.interface'</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">AuthService</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'@services/auth.service'</span>; <span class="hljs-keyword">import</span> { validate } <span class="hljs-keyword">from</span> <span class="hljs-string">'class-validator'</span>;

<span class="hljs-keyword">class</span> <span class="hljs-title class_">AuthController</span> { <span class="hljs-keyword">public</span> authService = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AuthService</span>();

<span class="hljs-keyword">public</span> signUp = <span class="hljs-keyword">async</span> (<span class="hljs-attr">req</span>: <span class="hljs-title class_">Request</span>, <span class="hljs-attr">res</span>: <span class="hljs-title class_">Response</span>, <span class="hljs-attr">next</span>: <span class="hljs-title class_">NextFunction</span>): <span class="hljs-title class_">Promise</span><<span class="hljs-built_in">void</span>> => { <span class="hljs-keyword">try</span> { <span class="hljs-keyword">const</span> createUserDto = <span class="hljs-keyword">new</span> <span class="hljs-title class_">CreateUserDto</span>(); createUserDto.<span class="hljs-property">email</span> = req.<span class="hljs-property">body</span>.<span class="hljs-property">email</span>; createUserDto.<span class="hljs-property">name</span> = req.<span class="hljs-property">body</span>.<span class="hljs-property">name</span>; createUserDto.<span class="hljs-property">password</span> = req.<span class="hljs-property">body</span>.<span class="hljs-property">password</span>;

  <span class="hljs-keyword">const</span> errors = <span class="hljs-keyword">await</span> <span class="hljs-title function_">validate</span>(createUserDto);
  <span class="hljs-keyword">if</span> (errors.<span class="hljs-property">length</span> &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">const</span> constraints = {};
    errors.<span class="hljs-title function_">forEach</span>(<span class="hljs-function"><span class="hljs-params">error</span> =&gt;</span> {
      <span class="hljs-keyword">const</span> propertyName = error.<span class="hljs-property">property</span>;
      <span class="hljs-keyword">const</span> err

Options

orConstraints = <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">values</span>(error.<span class="hljs-property">constraints</span>); constraints[propertyName] = errorConstraints; }); res.<span class="hljs-title function_">status</span>(<span class="hljs-number">400</span>).<span class="hljs-title function_">json</span>({ constraints }); <span class="hljs-keyword">return</span>; } <span class="hljs-keyword">const</span> <span class="hljs-attr">userData</span>: <span class="hljs-title class_">CreateUserDto</span> = req.<span class="hljs-property">body</span>; <span class="hljs-keyword">const</span> <span class="hljs-attr">signUpUserData</span>: <span class="hljs-title class_">User</span> = <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">authService</span>.<span class="hljs-title function_">signup</span>(userData);

  res.<span class="hljs-title function_">status</span>(<span class="hljs-number">201</span>).<span class="hljs-title function_">json</span>({ <span class="hljs-attr">data</span>: signUpUserData, <span class="hljs-attr">message</span>: <span class="hljs-string">'signup'</span> });
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-title function_">next</span>(error);
}

};

<span class="hljs-keyword">public</span> logIn = <span class="hljs-keyword">async</span> (<span class="hljs-attr">req</span>: <span class="hljs-title class_">Request</span>, <span class="hljs-attr">res</span>: <span class="hljs-title class_">Response</span>, <span class="hljs-attr">next</span>: <span class="hljs-title class_">NextFunction</span>): <span class="hljs-title class_">Promise</span><<span class="hljs-built_in">void</span>> => { <span class="hljs-keyword">try</span> { <span class="hljs-keyword">const</span> <span class="hljs-attr">userData</span>: <span class="hljs-title class_">CreateUserDto</span> = req.<span class="hljs-property">body</span>; <span class="hljs-keyword">const</span> { cookie, findUser } = <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">authService</span>.<span class="hljs-title function_">login</span>(userData);

  res.<span class="hljs-title function_">setHeader</span>(<span class="hljs-string">'Set-Cookie'</span>, [cookie]);
  res.<span class="hljs-title function_">status</span>(<span class="hljs-number">200</span>).<span class="hljs-title function_">json</span>({ <span class="hljs-attr">data</span>: findUser, <span class="hljs-attr">message</span>: <span class="hljs-string">'login'</span> });
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-title function_">next</span>(error);
}

};

<span class="hljs-keyword">public</span> logOut = <span class="hljs-keyword">async</span> (<span class="hljs-attr">req</span>: <span class="hljs-title class_">RequestWithUser</span>, <span class="hljs-attr">res</span>: <span class="hljs-title class_">Response</span>, <span class="hljs-attr">next</span>: <span class="hljs-title class_">NextFunction</span>): <span class="hljs-title class_">Promise</span><<span class="hljs-built_in">void</span>> => { <span class="hljs-keyword">try</span> { <span class="hljs-keyword">const</span> <span class="hljs-attr">userData</span>: <span class="hljs-title class_">User</span> = req.<span class="hljs-property">user</span>; <span class="hljs-keyword">const</span> <span class="hljs-attr">logOutUserData</span>: <span class="hljs-title class_">User</span> = <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">authService</span>.<span class="hljs-title function_">logout</span>(userData);

  res.<span class="hljs-title function_">setHeader</span>(<span class="hljs-string">'Set-Cookie'</span>, [<span class="hljs-string">'Authorization=; Max-age=0'</span>]);
  res.<span class="hljs-title function_">status</span>(<span class="hljs-number">200</span>).<span class="hljs-title function_">json</span>({ <span class="hljs-attr">data</span>: logOutUserData, <span class="hljs-attr">message</span>: <span class="hljs-string">'logout'</span> });
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-title function_">next</span>(error);
}

}; }

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title class_">AuthController</span>;</pre></div><p id="ec01">Before we create our auth routes, we want to create a new middleware, so that we can easily reuse it to secure all the routes that we want to secure with one simple command. Go ahead and create a new directory called <code>middlewares</code> and a new file called <code>auth.middleware.ts</code> with the following code:</p><div id="f7fc"><pre><span class="hljs-keyword">import</span> { <span class="hljs-title class_">NextFunction</span>, <span class="hljs-title class_">Response</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>; <span class="hljs-keyword">import</span> { verify } <span class="hljs-keyword">from</span> <span class="hljs-string">'jsonwebtoken'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">PrismaClient</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@prisma/client'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">HttpException</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@exceptions/HttpException'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">DataStoredInToken</span>, <span class="hljs-title class_">RequestWithUser</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@interfaces/auth.interface'</span>; <span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;

dotenv.<span class="hljs-title function_">config</span>();

<span class="hljs-keyword">const</span> <span class="hljs-variable constant_">SECRET_KEY</span> = process.<span class="hljs-property">env</span>.<span class="hljs-property">SECRET_KEY</span>;

<span class="hljs-keyword">const</span> <span class="hljs-title function_">authMiddleware</span> = <span class="hljs-keyword">async</span> (<span class="hljs-params">req: RequestWithUser, res: Response, next: NextFunction</span>) => { <span class="hljs-keyword">try</span> { <span class="hljs-keyword">const</span> <span class="hljs-title class_">Authorization</span> = req.<span class="hljs-property">cookies</span>[<span class="hljs-string">'Authorization'</span>] || (req.<span class="hljs-title function_">header</span>(<span class="hljs-string">'Authorization'</span>) ? req.<span class="hljs-title function_">header</span>(<span class="hljs-string">'Authorization'</span>).<span class="hljs-title function_">split</span>(<span class="hljs-string">'Bearer '</span>)[<span class="hljs-number">1</span>] : <span class="hljs-literal">null</span>);

<span class="hljs-keyword">if</span> (<span class="hljs-title class_">Authorization</span>) {
  <span class="hljs-keyword">const</span> <span class="hljs-attr">secretKey</span>: <span class="hljs-built_in">string</span> = <span class="hljs-variable constant_">SECRET_KEY</span>;
  <span class="hljs-keyword">const</span> verificationResponse = (<span class="hljs-keyword">await</span> <span class="hljs-title function_">verify</span>(<span class="hljs-title class_">Authorization</span>, secretKey)) <span class="hljs-keyword">as</span> <span class="hljs-title class_">DataStoredInToken</span>;
  <span class="hljs-keyword">const</span> userId = verificationResponse.<span class="hljs-property">id</span>;

  <span class="hljs-keyword">const</span> users = <span class="hljs-keyword">new</span> <span class="hljs-title class_">PrismaClient</span>().<span class="hljs-property">user</span>;
  <span class="hljs-keyword">const</span> findUser = <span class="hljs-keyword">await</span> users.<span class="hljs-title function_">findUnique</span>({ <span class="hljs-attr">where</span>: { <span class="hljs-attr">id</span>: userId } });

  <span class="hljs-keyword">if</span> (findUser) {
    req.<span class="hljs-property">user</span> = findUser;
    <span class="hljs-title function_">next</span>();
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>(<span class="hljs-number">401</span>, <span class="hljs-string">'Wrong authentication token'</span>));
  }
} <span class="hljs-keyword">else</span> {
  <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>(<span class="hljs-number">404</span>, <span class="hljs-string">'Authentication token missing'</span>));
}

} <span class="hljs-keyword">catch</span> (error) { <span class="hljs-title function_">next</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">HttpException</span>(<span class="hljs-number">401</span>, <span class="hljs-string">'Wrong authentication token'</span>)); } };

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> authMiddleware;</pre></div><p id="06c6">Now we can create the <code>auth.routes.ts</code> file:</p><div id="50e7"><pre><span class="hljs-keyword">import</span> { <span class="hljs-title class_">Router</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">AuthController</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'@controllers/auth.controller'</span>; <span class="hljs-keyword">import</span> authMiddleware <span class="hljs-keyword">from</span> <span class="hljs-string">'@middlewares/auth.middleware'</span>;

<span class="hljs-keyword">const</span> authRouter = <span class="hljs-title class_">Router</span>(); <span class="hljs-keyword">const</span> authController = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AuthController</span>();

authRouter.<span class="hljs-title function_">post</span>(<span class="hljs-string">'/signup'</span>, authController.<span class="hljs-property">signUp</span>);

authRouter.<span class="hljs-title function_">post</span>(<span class="hljs-string">'/login'</span>, authController.<span class="hljs-property">logIn</span>);

authRouter.<span class="hljs-title function_">post</span>(<span class="hljs-string">'/logout'</span>, authMiddleware, authController.<span class="hljs-property">logOut</span>);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> authRouter;</pre></div><p id="f63a">And use it in our <code>index.ts</code> file:</p><div id="2fb0"><pre><span class="hljs-keyword">import</span> authRouter <span class="hljs-keyword">from</span> <span class="hljs-string">'@/routers/auth.routes'</span>;</pre></div><div id="21f7"><pre>app.<span class="hljs-title function_">use</span>(<span class="hljs-string">'/'</span>, authRouter);</pre></div><p id="7e53">Great! Now we are ready to secure our endpoints! As we have now created a Signup route, we can now comment out or delete the <code>createUser</code> route.</p><div id="ee9a"><pre><span class="hljs-comment">//usersRouter.post('/users', usersController.createUser);</span></pre></div><p id="42cf">If we now want to secure specific routes, we simply have to add our auth middleware. Let’s import it in our <code>users.routes.ts</code> file and secure all of the routes:</p><div id="8b34"><pre><span class="hljs-keyword">import</span> { <span class="hljs-title class_">Router</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">UsersController</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'@controllers/users.controller'</span>; <span class="hljs-keyword">import</span> authMiddleware <span class="hljs-keyword">from</span> <span class="hljs-string">'@middlewares/auth.middleware'</span>;

<span class="hljs-keyword">const</span> usersRouter = <span class="hljs-title class_">Router</span>(); <span class="hljs-keyword">const</span> usersController = <span class="hljs-keyword">new</span> <span class="hljs-title class_">UsersController</span>();

usersRouter.<span class="hljs-title function_">get</span>(<span class="hljs-string">'/users'</span>, authMiddleware, usersController.<span class="hljs-property">getUsers</span>);

usersRouter.<span class="hljs-title function_">get</span>(<span class="hljs-string">'/users/:id'</span>, authMiddleware, usersController.<span class="hljs-property">getUserById</span>);

<span class="hljs-comment">//usersRouter.post('/users', usersController.createUser);</span>

usersRouter.<span class="hljs-title function_">put</span>(<span class="hljs-string">'/users/:id'</span>, authMiddleware, usersController.<span class="hljs-property">updateUser</span>);

usersRouter.<span class="hljs-title function_">delete</span>(<span class="hljs-string">'/users/:id'</span>, authMiddleware, usersController.<span class="hljs-property">deleteUser</span>);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> usersRouter;</pre></div><p id="295a">To test it, we are going to make use of some features in Postman in the next section.</p><p id="7d07">To get a response with the error message, we also want to create an error middleware. Go ahead and create a new file called <code>error.middleware.ts</code> in the <code>middleware</code> directory with the following code:</p><div id="c755"><pre><span class="hljs-keyword">import</span> { <span class="hljs-title class_">NextFunction</span>, <span class="hljs-title class_">Request</span>, <span class="hljs-title class_">Response</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>; <span class="hljs-keyword">import</span> { <span class="hljs-title class_">HttpException</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'@exceptions/HttpException'</span>;

<span class="hljs-keyword">const</span> <span class="hljs-title function_">errorMiddleware</span> = (<span class="hljs-params">error: HttpException, req: Request, res: Response, next: NextFunction</span>) => { <span class="hljs-keyword">try</span> { <span class="hljs-keyword">const</span> <span class="hljs-attr">status</span>: <span class="hljs-built_in">number</span> = error.<span class="hljs-property">status</span> || <span class="hljs-number">500</span>; <span class="hljs-keyword">const</span> <span class="hljs-attr">message</span>: <span class="hljs-built_in">string</span> = error.<span class="hljs-property">message</span> || <span class="hljs-string">'Something went wrong'</span>;

res.<span class="hljs-title function_">status</span>(status).<span class="hljs-title function_">json</span>({ message });

} <span class="hljs-keyword">catch</span> (error) { <span class="hljs-title function_">next</span>(error); } };

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> errorMiddleware;</pre></div><p id="629e">Now use it in the <code>index.ts</code> file:</p><div id="46af"><pre><span class="hljs-keyword">import</span> errorMiddleware <span class="hljs-keyword">from</span> <span class="hljs-string">'@middlewares/error.middleware'</span>;</pre></div><div id="4e05"><pre>app.<span class="hljs-title function_">use</span>(errorMiddleware);</pre></div><h1 id="f92e">Advanced Postman Features</h1><p id="c931">In this section, we are going to make use of some of the advanced features of Postman.</p><p id="92cd">The first feature that we are going to use are Environments. Let’s go ahead and create a new Environment called <code>DEV: expressAPI</code>.</p><blockquote id="37e8"><p>The main reason why we call this DEV: is, because with Postman environments, you can quickly switch between different sets of variables. This is useful when you need to test your API against different environments, such as development, staging, or production.</p></blockquote><p id="a4bf">We will use it to create a variable for URL and JWT as shown in the following image.</p><figure id="2e10"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*ACY_BMC0sujMhwfnQtq1nA.png"><figcaption></figcaption></figure><p id="740c">You can now use the URL variable in your requests by using {{URL}} as shown in the following. Make sure to select the correct environment in your request as well as you can see in the top right corner. You can go ahead and do this for all of your requests.</p><figure id="f533"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*hObnXzwQ2FTHk8Vbmxgl-A.png"><figcaption></figcaption></figure><p id="9f70">Let’s also create a request for Login and Logout as shown above.</p><p id="b1ad">Login:</p><figure id="6f9b"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*gDe1O_h4V0NP6VZOAzWcag.png"><figcaption></figcaption></figure><p id="084b">Logout:</p><figure id="8d16"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*OykdFGTv1gVJ51nbCSYKvA.png"><figcaption></figcaption></figure><p id="6484">With implementing authentication, testing our API has become more challenging. We can no longer can test the endpoints without being authenticated. In order that we do not have do login and copy the JWT token to the request so that we can send an authenticated request, we already made a good decision and decided to use cookies for authentication.</p><blockquote id="caf2"><p>However, I quickly want to show you how you can automate this process in Postman when using a Bearer token that you receive as json.</p></blockquote><blockquote id="3580"><p>You can set the following command in your request under Tests so that the access_token that comes as json is automatically stored in an environment variable called JWT that we created.</p></blockquote><blockquote id="322b"><p><code>pm.environment.set("JWT”,pm.response.sjon().access_token);</code></p></blockquote><blockquote id="cd37"><p>Now you can use the access token in other requests inside the Auth tab as type Bearer Token like this {{JWT}}</p></blockquote><p id="48db">After login, you can check the tab Cookies and see that we got our JWT token as value and we can also see when it was created in the Expires column.</p><figure id="7acc"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*83Jb0QXKCW5wCqf2P4LnAA.png"><figcaption></figcaption></figure><p id="997b">After a successful login, you can test all of the secured API endpoints without any problem.</p><h1 id="a6f0">Final Thoughts</h1><p id="5035">There are still some final steps that you could do before deploying to production. At this point you have a good understanding of how an express API works under the hood. You can go ahead and check out this <a href="https://github.com/ljlm0402/typescript-express-starter/tree/master/lib">github repo</a>, that also includes features that we did not cover. There you can find complete production ready project starters for different ORMs. I hope this series provided enough information so that you can go ahead and use these project starters on your own and understand the structure, as well as all the features that are implemented. The project starters also include the following:</p><ul><li>Object Oriented Approach</li><li>Create other Reusable Middleware</li><li>Sophisticated Error Handling</li><li>Testing with Jest</li><li>Documentation with SwaggerUI</li><li>Docker and PM2</li></ul><p id="5999">You can also find complete project starters for different ORMs including:</p><ul><li>graphql</li><li>knex</li><li>mikro-orm</li><li>mongoose</li><li>prisma</li><li>routing-controllers</li><li>sequelize</li><li>typegoose</li><li>typeorm</li></ul><p id="72af">This series was meant to teach you everything that you need to know to understand how an express API works under the hood. Now you are able to use the different project starters from the <a href="https://github.com/ljlm0402/typescript-express-starter/tree/master/lib">github repo</a> mentioned above.</p><p id="35d7">If you want to explore API Development with python and fastapi, I can recommend the following tutorial. It is a great course if you want to know everything from start to finish and you want a video that you can follow. It includes everything, even how to dockerize your application, how to deploy it and a CI/CD pipeline as well. You can also use some parts for our API project here.</p> <figure id="4167"> <div> <div> <img class="ratio" src="http://placehold.it/16x9"> <iframe class="" src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F0sOvCWFmrtA%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0sOvCWFmrtA&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2F0sOvCWFmrtA%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" allowfullscreen="" frameborder="0" height="480" width="854"> </div> </div> </figure></iframe></div></div></figure><p id="cb85">I hope you find this series helpful, feel free to clap and follow me to stay up to date with all the articles from myself. More tutorials and insights are on the way.</p><p id="d781">If you want to get unlimited, ad-free access to all the stories on Medium, you can subscribe by using my referral link below :)</p><div id="d0a3" class="link-block"> <a href="https://medium.com/@wimluk/membership"> <div> <div> <h2>Join Medium with my referral link - Lukas Wimhofer</h2> <div><h3>Get unlimited, ad-free access to all the stories on Medium. Your membership fee directly supports Lukas Wimhofer and…</h3></div> <div><p>medium.com</p></div> </div> <div> <div style="background-image: url(https://miro.readmedium.com/v2/resize:fit:320/0*c7jIMmjx-URJM2Ia)"></div> </div> </div> </a> </div></article></body>

API development with nodejs, express and typescript from scratch — DTO, Interface and Authentication

This is the fifth part of the series “API development with nodejs, express and typescript from scratch” and it is all about the structure of data and security.

We will cover the following steps:

  • Schema Validation
  • DTOs
  • Interfaces
  • Authentication
  • Advanced Postman Features
  • Final Thoughts

We will continue building our API that we started in a previous part of the series. If you have not seen the previous parts, follow the link below to get an overview. You can also check out github, clone the individual parts of the series from the github repository and use npm install to get started.

Make sure to set everything up and without further ado, let’s get started.

Schema Validation

Schema validation is the process of checking if the data sent to an API endpoint (such as a request body) conforms to a specific structure or format. This helps ensure that the API only receives valid data, and can also help to prevent security issues such as SQL injection.

TypeScript can be used in conjunction with Express and the class-validator library to provide type checking and improved code-completion for your schema definitions. This can help to catch errors early in the development process, and make it easier to write and maintain your code.

In the context of schema validation, DTOs (Data Transfer Objects) and interfaces serve different purposes but are related in that they both describe the structure of data in your application.

While DTOs are used to define the structure of data that is sent to or received from an API endpoint, interfaces provide a clear contract between the different parts inside of your application.

Let’s have a look on what they are and how we can implement DTOs and interfaces in our application.

DTOs

A DTO (Data Transfer Object) is an object that defines the structure of data that is sent to an API endpoint. DTOs are used to ensure that data sent to the API conforms to a specific structure, and to provide a clear contract between the API and its clients.

By using DTOs, you can ensure that data sent to the API is in the correct format and contains all the necessary information, and that data received from the API can be processed correctly by the client.

We will be using a library called class-validator. Let’s install them first:

npm i class-validator

Class-validator allows the use of decorator and non-decorator based validation.

To keep our structure well organized, we are creating a new directory called dtos under src/ and a new file called users.dto.ts :

src/dtos/users.dto.ts

We will import the class-validator and create a new “Data Transfer Object”:

import { IsString, IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsNotEmpty()
  name: string;
}

In TypeScript, both classes and interfaces can be used to define Data Transfer Objects (DTOs). The choice between using classes or interfaces will depend on the specific requirements of your application.

Interfaces are commonly used to define DTOs because they allow you to describe the structure of an object without providing an implementation for its methods. Interfaces are a lightweight way to enforce type safety in your code, and to provide a clear contract between different parts of your application.

Classes, on the other hand, can provide more functionality than interfaces, as they allow you to define both the structure and implementation of an object. Classes are often used when you need to define complex data structures that have methods or other behavior.

We are using a class to define our dto, because we also use some class validator decorators.

In general, if you only need to describe the structure of a DTO, an interface is the preferred choice. However, if you need to add methods or other behavior to your DTO, a class may be a better option.

Until now, if we create a new user, we do not have to provide a valid email. You can try it by using any other string. However, this is why we defined our dto. To make use of our dto we have to make some changes to our code.

We will implement our validation in our users.controller.ts file so that we can pass the validated inputs to our services. Let’s import the following to our controller:

import { CreateUserDto } from '@/dtos/users.dto';
import { validate } from 'class-validator';

Now we can perform the validation with the validate function from class-validator like this:

  public createUser = async (req: Request, res: Response): Promise<void> => {
    try {
      const createUserDto = new CreateUserDto();
      createUserDto.email = req.body.email;
      createUserDto.name = req.body.name;

      const errors = await validate(createUserDto);
      if (errors.length > 0) {
        res.status(400).json({ errors });
        return;
      }

      const user: User = req.body;
      const newUser: User = await this.usersService.createUser(user);
      res.status(201).json({ user: newUser });
    } catch (e) {
      res.status(500).json({ error: e.message });
    }
  };

This approach separates the validation logic into a separate DTO class, which can be reused across your application, and keeps your controller and service code focused on their respective responsibilities.

When we test our validation by sending the following request:

{
    "email": "useremail.com"
}

We get the following response:

{
    "errors": [
        {
            "target": {
                "email": "useremail.com"
            },
            "value": "useremail.com",
            "property": "email",
            "children": [],
            "constraints": {
                "isEmail": "email must be an email"
            }
        },
        {
            "target": {
                "email": "useremail.com"
            },
            "property": "name",
            "children": [],
            "constraints": {
                "isNotEmpty": "name should not be empty",
                "isString": "name must be a string"
            }
        }
    ]
}

As you can see, we are validating the email and the name property and the validate function also returns all the constraints. We can also access the the ValidationErrors array and creating a new object with the property as the key and the constraints as the value. Like this, we can define how the response should look like:

  public createUser = async (req: Request, res: Response): Promise<void> => {
    try {
      const createUserDto = new CreateUserDto();
      createUserDto.email = req.body.email;
      createUserDto.name = req.body.name;

      const errors = await validate(createUserDto);
      if (errors.length > 0) {
        const constraints = {};
        errors.forEach(error => {
          const propertyName = error.property;
          const errorConstraints = Object.values(error.constraints);
          constraints[propertyName] = errorConstraints;
        });
        res.status(400).json({ constraints });
        return;
      }

If we send the invalid request from above again, we now get this response:

{
    "constraints": {
        "email": [
            "email must be an email"
        ],
        "name": [
            "name should not be empty",
            "name must be a string"
        ]
    }
}

Interfaces

An interface, on the other hand, is a way to define a custom type in TypeScript. Interfaces are used to define the structure of objects and ensure that they conform to a specific shape.

When you write TypeScript code, the TypeScript compiler compiles your code into JavaScript. During this compilation process, the TypeScript compiler takes into account the interfaces you have defined, and generates JavaScript code that implements the behavior described by those interfaces.

However, it’s important to note that the interfaces themselves are not present in the generated JavaScript code. The JavaScript code generated by the TypeScript compiler only implements the behavior defined by the interfaces, but the interfaces themselves are not part of the generated code.

This means that, when you run the generated JavaScript code, it will not have any knowledge of the interfaces you defined in your TypeScript code, keeping the bundle smaller compared to classes. However, the generated JavaScript code will still have the behavior that was described by the interfaces, and will still be type-safe and correctly implement the contracts defined by the interfaces.

At this point, when we create a new user, we return the whole object including the generated id. If we do not want to return the id for example, we can make use of interfaces to define how the response should look like.

Let’s start by creating a new directory called interfaces in our src directory and create a new file called users.interface.ts like so:

src/interfaces/users.interface.ts

Let’s define our CreateUserResponse interface:

export interface CreateUserResponse {
  email: string;
  name: string;
}

Now we can import it and use it like this in our users.controller.ts file :

import { CreateUserResponse } from '@/interfaces/users.interface';
  public createUser = async (req: Request, res: Response): Promise<void> => {
    try {
      const createUserDto = new CreateUserDto();
      createUserDto.email = req.body.email;
      createUserDto.name = req.body.name;

      const errors = await validate(createUserDto);
      if (errors.length > 0) {
        const constraints = {};
        errors.forEach(error => {
          const propertyName = error.property;
          const errorConstraints = Object.values(error.constraints);
          constraints[propertyName] = errorConstraints;
        });
        res.status(400).json({ constraints });
        return;
      }

      const user: User = req.body;
      const newUser: User = await this.usersService.createUser(user);
      const { email, name } = newUser;
      const createUserResponse: CreateUserResponse = { email, name };
      res.status(201).json({ user: createUserResponse });
    } catch (e) {
      res.status(500).json({ error: e.message });
    }
  };

When we now create a new user, the response will look like this:

{
    "user": {
        "email": "[email protected]",
        "name": "user"
    }
}

Authentication

At the moment, we can call all of our API endpoints without authentication. However, we want to secure some of the endpoints so that you can only call the API when you are authenticated.

In an Express application, authentication typically involves the following steps:

  1. User provides their credentials (e.g. username and password) to the server via a login form or API endpoint.
  2. The server verifies the user’s credentials, usually by checking them against a database of user accounts.
  3. If the credentials are valid, the server generates an authentication token (e.g. a JSON Web Token or JWT) and sends it to the client.
  4. The client stores the token (e.g. in a cookie or in local storage) and sends it with each subsequent request to the server.
  5. The server checks the validity of the token on each request and grants access to protected resources only if the token is valid.

First we need to edit our user model so that we can authenticate users by using a password.

Let’s edit our schema.primsa file:

model User {
  id      String   @id @default(auto()) @map("_id") @db.ObjectId
  email   String   @unique
  name    String
  password String
}

Now use this command to generate the new client:

npx prisma generate

Note that if the type User is not updated in other files after importing, close and open the editor again.

Let’s also add the password to our Dto:

  @IsString()
  @IsNotEmpty()
  password: string;

Next we create a new interface called auth.interface.ts where we define the structures that we need for our auth service:

import { Request } from 'express';
import { User } from '@prisma/client';

export interface DataStoredInToken {
  id: string;
}

export interface TokenData {
  token: string;
  expiresIn: number;
}

export interface RequestWithUser extends Request {
  user: User;
}

The reason you need this interface is that you want to attach additional properties to the standard Request object in Express.js. In this case, you want to attach a user property to the Request object that represents the authenticated user making the request.

The RequestWithUser interface extends the Request interface from the express module, and adds a user property of type User from the Prisma client. This allows you to access the user property in subsequent middleware or route handlers, without having to typecast the Request object every time.

The DataStoredInToken and TokenData interfaces are used to define the structure of the JSON Web Token that is generated and returned to the client during authentication.

DataStoredInToken defines the structure of the data that will be stored in the token, which in this case is just the id of the user.

TokenData defines the structure of the JSON Web Token itself, which consists of a token property containing the actual token string, and an expiresIn property indicating the expiration time of the token in seconds. These interfaces are used by the createToken method in your AuthService to generate and return the JWT token.

Let’s also define our a new class called HttpException . For this we create a new directory called exceptions and a new file called HttpException.ts with the following code:

export class HttpException extends Error {
  public status: number;
  public message: string;

  constructor(status: number, message: string) {
    super(message);
    this.status = status;
    this.message = message;
  }
}

The HttpException class allows you to define a consistent structure for your errors, by including a status property that represents the HTTP status code to send in the response, and a message property that represents the error message to send. By creating custom errors that conform to this structure, you can ensure that your error-handling middleware can easily handle and respond to errors in a consistent and predictable way.

Before we create the auth service, we want to install the libraries that we need for authenticating users with JWT:

npm i jsonwebtoken bcrypt cookie-parser

jsonwebtoken: This package is used for generating and verifying JSON Web Tokens (JWT), which are a standard for securely transmitting information between parties. You can use JWT to implement stateless authentication in your application, where the client sends the JWT token with each request to access protected resources.

bcrypt: This package is used for hashing and verifying passwords, and is a popular way to securely store passwords in databases. When a user creates an account or resets their password, you can use bcrypt to hash the password and store the hash in the database. When the user logs in, you can use bcrypt to verify the password against the stored hash.

cookie-parser: This package is used for parsing cookies in the request headers, and makes it easier to work with cookies in your application. You can use cookies to implement server-side sessions and maintain the user's authentication state across requests.

We can go ahead and add the cookie parser to our index.ts file:

import cookieParser from 'cookie-parser';
// App Configuration

const app = express();

app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(cookieParser());

We will also need a SECRET_KEY env variable so go ahead and add it to the .env file:

SECRET_KEY=yoursecret

Also make sure to add to to our validation in validate.Env.ts :

 SECRET_KEY: str(),

Now let’s create our auth.service.ts file and define our services:

import { compare, hash } from 'bcrypt';
import { sign } from 'jsonwebtoken';
import { PrismaClient, User } from '@prisma/client';
import { CreateUserDto } from '@dtos/users.dto';
import { DataStoredInToken, TokenData } from '@interfaces/auth.interface';
import { HttpException } from '@exceptions/HttpException';
import * as dotenv from 'dotenv';

dotenv.config();

const SECRET_KEY = process.env.SECRET_KEY;

class AuthService {
  public users = new PrismaClient().user;

  public async signup(data: CreateUserDto): Promise<User> {
    const findUser: User = await this.users.findUnique({ where: { email: data.email } });
    if (findUser) throw new HttpException(409, `This email ${data.email} already exists`);

    const hashedPassword = await hash(data.password, 10);
    const createUserData: Promise<User> = this.users.create({ data: { ...data, password: hashedPassword } });

    return createUserData;
  }

  public async login(data: CreateUserDto): Promise<{ cookie: string; findUser: User }> {
    const findUser: User = await this.users.findUnique({ where: { email: data.email } });
    if (!findUser) throw new HttpException(409, `This email ${data.email} was not found`);

    const isPasswordMatching: boolean = await compare(data.password, findUser.password);
    if (!isPasswordMatching) throw new HttpException(409, 'Password is not matching');

    const tokenData = this.createToken(findUser);
    const cookie = this.createCookie(tokenData);

    return { cookie, findUser };
  }

  public async logout(data: User): Promise<User> {
    const findUser: User = await this.users.findFirst({ where: { email: data.email, password: data.password } });
    if (!findUser) throw new HttpException(409, "User doesn't exist");

    return findUser;
  }

  public createToken(user: User): TokenData {
    const dataStoredInToken: DataStoredInToken = { id: user.id };
    const secretKey: string = SECRET_KEY;
    const expiresIn: number = 60 * 60;

    return { expiresIn, token: sign(dataStoredInToken, secretKey, { expiresIn }) };
  }

  public createCookie(tokenData: TokenData): string {
    return `Authorization=${tokenData.token}; HttpOnly; Max-Age=${tokenData.expiresIn};`;
  }
}

export default AuthService;

Now we can create the auth.controller.ts file:

import { NextFunction, Request, Response } from 'express';
import { User } from '@prisma/client';
import { CreateUserDto } from '@dtos/users.dto';
import { RequestWithUser } from '@interfaces/auth.interface';
import AuthService from '@services/auth.service';
import { validate } from 'class-validator';

class AuthController {
  public authService = new AuthService();

  public signUp = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const createUserDto = new CreateUserDto();
      createUserDto.email = req.body.email;
      createUserDto.name = req.body.name;
      createUserDto.password = req.body.password;

      const errors = await validate(createUserDto);
      if (errors.length > 0) {
        const constraints = {};
        errors.forEach(error => {
          const propertyName = error.property;
          const errorConstraints = Object.values(error.constraints);
          constraints[propertyName] = errorConstraints;
        });
        res.status(400).json({ constraints });
        return;
      }
      const userData: CreateUserDto = req.body;
      const signUpUserData: User = await this.authService.signup(userData);

      res.status(201).json({ data: signUpUserData, message: 'signup' });
    } catch (error) {
      next(error);
    }
  };

  public logIn = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const userData: CreateUserDto = req.body;
      const { cookie, findUser } = await this.authService.login(userData);

      res.setHeader('Set-Cookie', [cookie]);
      res.status(200).json({ data: findUser, message: 'login' });
    } catch (error) {
      next(error);
    }
  };

  public logOut = async (req: RequestWithUser, res: Response, next: NextFunction): Promise<void> => {
    try {
      const userData: User = req.user;
      const logOutUserData: User = await this.authService.logout(userData);

      res.setHeader('Set-Cookie', ['Authorization=; Max-age=0']);
      res.status(200).json({ data: logOutUserData, message: 'logout' });
    } catch (error) {
      next(error);
    }
  };
}

export default AuthController;

Before we create our auth routes, we want to create a new middleware, so that we can easily reuse it to secure all the routes that we want to secure with one simple command. Go ahead and create a new directory called middlewares and a new file called auth.middleware.ts with the following code:

import { NextFunction, Response } from 'express';
import { verify } from 'jsonwebtoken';
import { PrismaClient } from '@prisma/client';
import { HttpException } from '@exceptions/HttpException';
import { DataStoredInToken, RequestWithUser } from '@interfaces/auth.interface';
import * as dotenv from 'dotenv';

dotenv.config();

const SECRET_KEY = process.env.SECRET_KEY;

const authMiddleware = async (req: RequestWithUser, res: Response, next: NextFunction) => {
  try {
    const Authorization = req.cookies['Authorization'] || (req.header('Authorization') ? req.header('Authorization').split('Bearer ')[1] : null);

    if (Authorization) {
      const secretKey: string = SECRET_KEY;
      const verificationResponse = (await verify(Authorization, secretKey)) as DataStoredInToken;
      const userId = verificationResponse.id;

      const users = new PrismaClient().user;
      const findUser = await users.findUnique({ where: { id: userId } });

      if (findUser) {
        req.user = findUser;
        next();
      } else {
        next(new HttpException(401, 'Wrong authentication token'));
      }
    } else {
      next(new HttpException(404, 'Authentication token missing'));
    }
  } catch (error) {
    next(new HttpException(401, 'Wrong authentication token'));
  }
};

export default authMiddleware;

Now we can create the auth.routes.ts file:

import { Router } from 'express';
import AuthController from '@controllers/auth.controller';
import authMiddleware from '@middlewares/auth.middleware';

const authRouter = Router();
const authController = new AuthController();

authRouter.post('/signup', authController.signUp);

authRouter.post('/login', authController.logIn);

authRouter.post('/logout', authMiddleware, authController.logOut);

export default authRouter;

And use it in our index.ts file:

import authRouter from '@/routers/auth.routes';
app.use('/', authRouter);

Great! Now we are ready to secure our endpoints! As we have now created a Signup route, we can now comment out or delete the createUser route.

//usersRouter.post('/users', usersController.createUser);

If we now want to secure specific routes, we simply have to add our auth middleware. Let’s import it in our users.routes.ts file and secure all of the routes:

import { Router } from 'express';
import UsersController from '@controllers/users.controller';
import authMiddleware from '@middlewares/auth.middleware';

const usersRouter = Router();
const usersController = new UsersController();

usersRouter.get('/users', authMiddleware, usersController.getUsers);

usersRouter.get('/users/:id', authMiddleware, usersController.getUserById);

//usersRouter.post('/users', usersController.createUser);

usersRouter.put('/users/:id', authMiddleware, usersController.updateUser);

usersRouter.delete('/users/:id', authMiddleware, usersController.deleteUser);

export default usersRouter;

To test it, we are going to make use of some features in Postman in the next section.

To get a response with the error message, we also want to create an error middleware. Go ahead and create a new file called error.middleware.ts in the middleware directory with the following code:

import { NextFunction, Request, Response } from 'express';
import { HttpException } from '@exceptions/HttpException';

const errorMiddleware = (error: HttpException, req: Request, res: Response, next: NextFunction) => {
  try {
    const status: number = error.status || 500;
    const message: string = error.message || 'Something went wrong';

    res.status(status).json({ message });
  } catch (error) {
    next(error);
  }
};

export default errorMiddleware;

Now use it in the index.ts file:

import errorMiddleware from '@middlewares/error.middleware';
app.use(errorMiddleware);

Advanced Postman Features

In this section, we are going to make use of some of the advanced features of Postman.

The first feature that we are going to use are Environments. Let’s go ahead and create a new Environment called DEV: expressAPI.

The main reason why we call this DEV: is, because with Postman environments, you can quickly switch between different sets of variables. This is useful when you need to test your API against different environments, such as development, staging, or production.

We will use it to create a variable for URL and JWT as shown in the following image.

You can now use the URL variable in your requests by using {{URL}} as shown in the following. Make sure to select the correct environment in your request as well as you can see in the top right corner. You can go ahead and do this for all of your requests.

Let’s also create a request for Login and Logout as shown above.

Login:

Logout:

With implementing authentication, testing our API has become more challenging. We can no longer can test the endpoints without being authenticated. In order that we do not have do login and copy the JWT token to the request so that we can send an authenticated request, we already made a good decision and decided to use cookies for authentication.

However, I quickly want to show you how you can automate this process in Postman when using a Bearer token that you receive as json.

You can set the following command in your request under Tests so that the access_token that comes as json is automatically stored in an environment variable called JWT that we created.

pm.environment.set("JWT”,pm.response.sjon().access_token);

Now you can use the access token in other requests inside the Auth tab as type Bearer Token like this {{JWT}}

After login, you can check the tab Cookies and see that we got our JWT token as value and we can also see when it was created in the Expires column.

After a successful login, you can test all of the secured API endpoints without any problem.

Final Thoughts

There are still some final steps that you could do before deploying to production. At this point you have a good understanding of how an express API works under the hood. You can go ahead and check out this github repo, that also includes features that we did not cover. There you can find complete production ready project starters for different ORMs. I hope this series provided enough information so that you can go ahead and use these project starters on your own and understand the structure, as well as all the features that are implemented. The project starters also include the following:

  • Object Oriented Approach
  • Create other Reusable Middleware
  • Sophisticated Error Handling
  • Testing with Jest
  • Documentation with SwaggerUI
  • Docker and PM2

You can also find complete project starters for different ORMs including:

  • graphql
  • knex
  • mikro-orm
  • mongoose
  • prisma
  • routing-controllers
  • sequelize
  • typegoose
  • typeorm

This series was meant to teach you everything that you need to know to understand how an express API works under the hood. Now you are able to use the different project starters from the github repo mentioned above.

If you want to explore API Development with python and fastapi, I can recommend the following tutorial. It is a great course if you want to know everything from start to finish and you want a video that you can follow. It includes everything, even how to dockerize your application, how to deploy it and a CI/CD pipeline as well. You can also use some parts for our API project here.

I hope you find this series helpful, feel free to clap and follow me to stay up to date with all the articles from myself. More tutorials and insights are on the way.

If you want to get unlimited, ad-free access to all the stories on Medium, you can subscribe by using my referral link below :)

Web Development
Programming
Expressjs
API
Recommended from ReadMedium