As I said yesterday, Swagger is a super helpful tool for auto-generating your API, however, it is not a silver bullet. There’s still a fair amount of work to do, and there’s not a ton of documentation on how the pre-built API chooses to handle its data!
So, today, I’m going to walk through how I got one of my routes totally up and running as expected!
Service vs Controller
I’ll be honest, these words didn’t mean anything that different to me before today. My understanding of them still might be a bit shotty, but I can tell you how the Swagger uses them.
You may remember that when you generate a server with Swagger/OpenAPI, you will end up with a few directories, two of them are services
and controllers
.
In the application flow, requests come in at the index.js
file, and then are passed into the controllers, which in turn, call their related services.
Controllers provide all of the communication for the route, they handle the request coming in, and passing the response back. The services provide the connection to external sources like a database pool.
This will be important to understand as we go back and forth between RoastService.js
and controllers/Roast.js
.
Setting Up RoastService.js
The service files, by default, are set to pretty generic functions that return promises and some mock data. Here’s an example of the service function the codegen outputted for my getUserRoasts
operation.
exports.getUserRoasts = function(username) {
return new Promise(function(resolve, reject) {
var examples = {};
examples['application/json'] = [ {
// mock roast data object 1
}, {
// mock roast data object 2
} ];
if (Object.keys(examples).length > 0) {
resolve(examples[Object.keys(examples)[0]]);
} else {
resolve();
}
});
}
All this function does is check to see if there are keys in the object examples
and then resolves the returned Promise with the examples
. If there are no keys, the Promise is resolved with no content.
This is not really what we want it to do. So, after importing our database connection as db
at the top level of the file, we can change the function to this.
const db = require('../database/db');
exports.getUserRoasts = function(username) {
return new Promise((resolve, reject) => {
db.query('SELECT id FROM users WHERE username = $1;', [username])
.then((response) => {
if (response.rowCount == 0) {
reject('User not found.')
}
const userId = response.rows[0].id;
return db.query('SELECT * FROM roasts WHERE user_id = $1;', [userId]);
})
.then((roastQueryResponse) => {
resolve(roastQueryResponse.rows);
})
.catch((error) => {
reject(error);
});
});
};
Let’s walk through the new function.
- Our function returns a promise that is created using the promise constructor.
- The database is queried using the
db.query
method which takes in the text of a query and an array of parameters. (It's important to parameterize our database queries to protect from SQL injection). -
db.query
returns a promise, so we use a.then
method to ensure that it has either resolved or rejected before the operation has moved on. -
.then
takes an anonymous function with the response from the database as an argument. - If the
rowCount
property of the response object is zero, then our query didn't retrieve a user. So we reject the promise with the message indicating that no user was found. - Database queries return information in rows, which can be accessed from the
rows
property of the response object. Since theusername
column is unique in the database, we will only ever get one result. So we extract theid
column value from the first index of therows
array. - To get to the array of roasts associated with the
id
of the user, we need to perform another query to get the array of roasts. - After that promise resolves, the response is passed to the next
.then
method, which resolved the whole promise to the value of therows
property in the second query's response object. - If at any point in the promise chain, an error is thrown, the
.catch
method will reject the returned promise with the error that stopped it.
As I’m writing this, I am realizing that I probably should have just used a JOIN to ensure that I only had to perform one database query. I have some refactoring to do tomorrow, but this will work for now.
Setting Up controller/Roast.js
So, now our service function is returning data, and in fact, if we were to use a tool like Postman to hit the API at this route with a valid username parameter, we would receive the array of roasts!
But, we’re not quite done yet.
Here’s the function that we start with in our controller:
module.exports.getUserRoasts = function getUserRoasts (req, res, next, username) {
Roast.getUserRoasts(username)
.then(function (response) {
utils.writeJson(res, response);
})
.catch(function (response) {
utils.writeJson(res, response);
});
};
This function simply calls the service function we wrote earlier, passing it the username that has been extracted through middleware already. After waiting, whether or not the promise resolves or rejects, it will call a utility function that turns that response into readable JSON.
That’s what we want, but its not enough yet. We also want to make sure that we’re handling our different error codes. Right now if we were to look for a non-existant user, we would still get our successful 200 status code, and an empty response.
We can fix this! To change the HTTP status code, the generated utility function writeJson
takes an optional third argument to specify the code!
module.exports.getUserRoasts = function getUserRoasts (req, res, next, username) {
Roast.getUserRoasts(username)
.then(function (response) {
if (!response) utils.writeJson(res, 'No roasts found.', 404);
utils.writeJson(res, response);
})
.catch(function (response) {
// Handle no user found.
utils.writeJson(res, response, 404);
});
};
We’ve actually handled two conditions here.
- User has no roasts. If a user doesn't have any roasts, the promise will still resolve, but it will contain an empty array, a falsy value. So we will respond with a 404 status code, since there's nothing here to find.
-
User can't be found. Since the promise will be rejected if the user can't be found, the
.catch
method will handle the response. Theresponse
parameter that's being passed in contains only the message ‘User not found.’ If you recall from the service, that is the value that the promise is rejected with.
Is it easier?
So, after spending my day trying to figure out the opinions of the Swagger generated API, I think it might save some time in the long run to have this framework built for you.
I like that I don’t have to handle all of my parameters and everything in neatly given to me. However, the lack of documentation on this part of the process left me scratching my head a time or two.
I’d say, if you don’t have a solid grasp of Express and how to create an api with it, start there. This is not an entry level, “make an api the easy way.” It still requires that you understand the ins and outs of the framework that you’re using.
Now that I’m more comfortable with some of the utility functions though, I feel prepared to make significant progress developing the routes tomorrow!
Check Out the Project
If you want to keep up with the changes, fork and run locally, or even suggest code changes, here’s a link to the GitHub repo!
https://github.com/nmiller15/roast
The frontend application is currently deployed on Netlify! If you want to mess around with some features and see it in action, view it on a mobile device below.
https://knowyourhomeroast.netlify.app
Note: This deployment has no backend api, so accounts and roasts are not actually saved anywhere between sessions.
Top comments (0)