Learn JS: FSFS – Section 12 – Creating an API


Courses

Updated Jul 5th, 2021

Section Table of Contents

Chapters 118 – Setting Up the Skeleton for an API

Chapters 119 – JSON Web Tokens (Part 1)

Chapters 120 – JSON Web Tokens (Part 2)

Chapters 121 – Finishing API and Understanding CORS

Go to the complex app summary

Chapter Details

Chapter 118 – Setting Up the Skeleton for an API

API stands for Application Programming Interface. Allows an application’s functionality accessible through programmatic requests, instead of being tightly coupled to a specific environment (currently the web browser).

Create UI that is not powered by html rendering engine. Expose data and functionality. You can use the free “postman” application/tool to send http requests. Send different types of request easily, add body field, JSON payload, headers, etc. It’s like a request playground.

As an example, use postman send a “POST” request to “http://localhost:3000/api/login.” Send some JSON data along with the request and to do that go to the “headers” tab in postman and set a key of “Content-Type” and value of “application/json.” On the “body” tab select the “raw” option and type in a bit of JSON. Remember that in JSON you need to wrap both your property names and values in quotes. Send an object with “username” of “brad” and “password” of “wrongpasswordonpurpose.”

Before clinking “send” to fire off the request, go to the “app.js” file to configure for a separate router file for api-related routes. Right under all of the requiring-in-packages lines, add app.use(‘/api’, require(‘./router-api)). Two lines that need to be above that code are the “app.use(urlencoded)” and “app.use(express.json)” lines.

// Here is where you require packages into app.js

app.use(express.urlencoded({extended: false}))
app.use(express.json())

app.use('/api', require('./router-api))

Create a new “router-api.js” file and setup a route for the “/api/login” route. Include an anonymous function for now instead of requiring in controllers. Copy and paste lines of code to bring in the controller files.

const apiRouter. require('express').Router()
// import three controller files here...

apiRouter.post('/login', userController.apiLogin)

module.exports = apiRouter

Make a new function in “userController.js” file called “exports.apiLogin” by duplicating the existing “exports.login” function and renaming. Decouple this new function from the web browser environment, (no flash messages, no redirecting, no sessions, etc.).

exports.apiLogin = function(req, res) {
  let user = new User(req.body)
  user.login().then(function(result) {
  res.json("Good Job, that is a valid username and password")
  }).catch(function(e) {
  res.json("Sorry, that your values are not correct.")
  })
}

Now send the postman request with the bogus password and be sure that it failed. Change the password to the correct one and make sure the request now succeeds.

Note that we do not need to add “/api” before our routes in the “router-api.js” file based on the way we set things up in the “app.js” file to have a prefix.

Chapter 119 – JSON Web Tokens (Part 2)

Install the “jsonwebtoken” package and require into the “userController.js” file. Customize the response for the request created in previous video.

res.json(jwt.sign(a, b, c))

Where a is any data we want to store in the token, passed in a data object.

b is secret phrase that the package uses when it generates the token. Put in directly or use an environment variable.

c is a configuration object. Can use any number and letter. For example, {expiresIn: 7d}.

res.json(jwt.sign({_id: user.data._id}, process.env.JWTSECRET, {expiresIn: 7d}))

Now the server responds to a request to “/api/login” with this JSON token string. It’s up to the application that is leveraging the API to store or save or hold on to the token so it can use it again in the future because the token proves to the server that you are the user that just logged in.

Set up a new route in “router.js” that would let users create a post at “/api/create-post.” Now instead of properties of “username” and “password” you would send properties of “title” and “body” (with corresponding values) in the request. Also include a “token” property and paste in your token. Sending this request from postman should create a new post in our app’s database and now visible on our site.

Chapter 120 – JSON Web Tokens (Part 2)

Set up the route for “/api/create-post” to run “userController.apiMustBeLoggedIn” and “postController.apiCreate.” Notice the slightly different name for the “mustBeLoggedIn” function. Duplicate the original “mustBeLoggedInFunction” and set up a try/catch block.

In the try block leverage the jwt.verify(a, b) method where a is the token to verify, (req.body.token), and b is the jwt secret phrase (process.env.JWTSECRET). Assign this to a variable using let called “req.apiUser.” Also call next(). In the catch respond by saying res.json(“Failed. Try again.”)

exports.apiMustBeLoggedIn = function(req, res, next) {
  req.apiUser = jwt.verify(req.body.token, process.env.JWTSECRET)
  next()
}

Note that when we create req.apiUser and call next(), the next function in the route would be able to access this req.apiUser variable, and so we can grab the user id.

Now build out the “exports.apiCreate” function by duplicating and reworking. Remove sessions and instead pass along newly a created “req.apiUser._id.” Strip out flash messaging and the manual saving of the session and just send back a simple JSON message using res.json(“Custom congrats message here”). Delete everything in the catch block and replace with a simple res.json(errors) to respond with our array of error.

exports.apiCreate = function(req, res) {
  let post = new Post(req.body, req.apiUser._id)
  post.create().then(function(newId) {
  res.json("Congrats.")
  }).catch(function(errors) {
  res.json(errors)
  })
}

So why in the hell are we trusting these JSON web tokens? We actually store data in the token and sign it, or give it a signature, with our super secret phrase. This makes it nearly cryptographically impossible for a malicious user to try and manipulate or try to edit their token to have someone else’s user id. The jwt.verify() method confirms that the token is valid and is one that we ourselves generated.

This is a big deal and web tokens are so popular because it allows for stateless authentication and we don’t need to keep track of session data on our server. This can greatly simplify and speed up what’s on our server. If we don’t need to store session data in memory or on the server in the database, that’s one less thing for us to manage. Tokens are great for APIs but may not be a perfect fit for everything. Sessions and tokens each have their pros and cons.

Chapter 121 – Finishing API and Understanding CORS

Set up a route to delete posts and implement CORS. Set up a “DELETE” request in postman to “localhost:3000/api/post/id-string-here” and send over the JSON web token.

apiRouter.delete('/post/:id', userController.apiMustBeLoggedIn, postController.apiDelete)

Note that a traditional html form can only send “post” and “get” request but when sending a request programmatically, or using an application like postman, you can send more types of requests, including a “DELETE” request.

Duplicate and tweak the “exports.delete” function to be a new “exports.apiDelete” function and gut out web browser-ish stuff (sessions, flash, redirects). Test that you cannot delete someone else’s post.

Note that you may see the benefit of the MVC model in that the we are creating api functions in the controller files but the functions (and logic) in the model files are still valid and don’t require any changes.

Now work on a request that should work for everyone, one that is publicly available.

apiRouter.get('/postsByAuthor/:username', userController.apiGetPostsByUsername)

Create “exports.apiGetPostsByUsername” with a try/catch block. (A quick aside: this function will await a promise called findByUsername which is declared in the User.js model file. In this project are there any promises declared in the controller? I will say no, but instead they get declared in the model files, and functions in the controller files work with the values those promises resolve or reject with).

When testing a publicly available route by sending a “get” request you don’t need to send along any JSON data in the request to the server.

Why do we need to enable CORS?

Browsers don’t allow “Cross Origin Resource Sharing,” or asynchronous requests to other domains, by default. A domain (website) can explicitly says that its okay to do so and this is what is known as “enabling CORS.”

// A typical axios get request that will fail without CORS enabled

axios.get('whatEvsUrl').then(function(response) {
  console.log(response.data)
  }).catch(function(err) {
  console.log(err)
  })

Install the “cors” package and in the “router-api.js” file require in the package and tells express to run it for all api routes (or the routes below this line):

const cors = require('cors')
apiRouter.use(cors())

Note that any routes below the “apiRoute.use(cors())” line will be “cors enabled” and so you can have some above and some below.

It’s usually a best practice to have a footer link to “API docs” explaining routes, and expected properties, and how the token system works, etc. If developers can send http requests (which is every major platform) then they are free to use the API.

Head to Section 13 – Deploying Complex App to Heroku