Topic Books and Reviews Aggregator: using React and ASP.NET Core consuming Google APIs
So I recently made an article on fetching a set of videos on many books on a certain topic I searched for for the following article, it is a must read if you want no confusion and understand what is going on: https://www.ramihikmat.net/Article/2021/topic-books-and-reviews-aggregator%3A-using-c%23-and-google%27s-books-and-youtube-apis
It served me well until I wanted to make a UI project so others can use it. That project was console based but here in this article will create a backend that will use what I made in the former article and create a frontend UI that will consume the API. Will be using React for the frontend, it is the market leader of Single Page Applications. Will use ASP.NET Core for the backend, since it is written in C# in the same framework as the former article’s project.
All the source code is here (including the former article’s): https://github.com/Morr0/Book-Reviews-Fetcher
I assume you know C# and Node.js to use React. Will obviously skip basics. Buckle up and let’s start.
Basically will be using the Google Books API and YouTube Data API. Please sign up with Google on the Google Developer Console to obtain your API key.
Coding the API project:
Go to the root directory of the solution and type in the following to create a new API project:
dotnet new webapp BookReview.WebApi
Now I am Windows thus why ..\ instead of ../ in file path for other operating systems.
Will move into the project directory and a reference to the console project. Typically you do reference libraries, however since everything is a DLL you can reference whatever you want in the .NET ecosystem.
cd ./BookReview.WebApi
dotnet add reference ../BookReview.App/BookReview.App.csproj
You can now check BookReview.WebApi.csproj to see it has the following reference:
<ItemGroup>
<ProjectReference Include="..\BookReview.App\BookReview.App.csproj" />
</ItemGroup>
Will delete default classes. Will go to the Startup class and design it to our needs. Will remove default services and add a Cors policy so we later allow the React app to be able to consume the server:
services.AddCors(x =>
{
x.AddPolicy(x.DefaultPolicyName, policy =>
{
policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin();
});
});
You can customize it to your needs. Basically Cors stands for Cross-Origin Resource Sharing, it is a browser only mechanism to reinforce safety in websites due to hacks coming from Cross-Site Scripting and others. Every single new HTTP connection to a website, on the first call, the browser will send an Options HTTP method request to get if any/certain/none resources can be consumed by the client. Please research more about it if interested. It generally traps beginner programmers into thinking what went wrong.
Now will configure the pipeline, will add the in the Configure method the use of Cors:
app.UseCors();
Please put it in really early on in the pipeline to immediately reject un-allowed requests.
Will remove app.UseHttpsRedirection() as well for development reasons.
Creating the controller in the Controllers directory named BooksReviewsController as follows:
using System.Collections.Generic;
using System.Threading.Tasks;
using BookReview.App.Abstractions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
namespace BookReview.WebApi.Controllers
{
[ApiController]
[Route("Book/Reviews")]
public class BooksReviewsController : ControllerBase
{
}
}
It is listening on base route of Book/Reviews. Note I am using the ApiController attribute to add some behaviour we don’t have to implement in every action method. This following article does a good job of explaining why:
https://www.strathweb.com/2018/02/exploring-the-apicontrollerattribute-and-its-features-for-asp-net-core-mvc-2-1/
Before creating the action method, will create the request model with validation named GetBookReviewsRequest:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BookReview.WebApi.Controllers
{
public class GetBookReviewsRequest : IValidatableObject
{
[Required] public string Topic { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
bool isMultiWord = Topic.Split().Length > 1;
if (isMultiWord) yield return new ValidationResult("Please only enter one word",
new[] {nameof(Topic)});
}
}
}
Here I am checking that the Topic string must not be null and then checking that it is only one word. You can take the one word checking off if you want to search in multiple words, I am fine with one word. If you take it off the class will look like the following:
using System.ComponentModel.DataAnnotations;
namespace BookReview.WebApi.Controllers
{
public class GetBookReviewsRequest
{
[Required] public string Topic { get; set; }
}
}
Less hassle.
Now ASP.NET Core comes installed with configuration packages, will add my API key to appsettings.json, if not created please create it, as I am anti hardcoding secrets in code.
{
"GoogleApiKey": "<API Key goes here>"
}
Then will create the action method, handling GET request on the base controller path:
[HttpGet]
public async Task<IActionResult> Get([FromServices] IConfiguration configuration, [FromQuery] GetBookReviewsRequest request)
{
string googleApiKey = configuration.GetSection("GoogleApiKey").Value;
// Coming Soon
}
This won’t compile because of not returning anything. However, here I am using dependency injection to inject the IConfiguration to read the Google API key like above. Also model binding to the query. If you want to enforce query validation it has to be in the model like above where I showed request model. That is [FromQuery, Required] string Topic won’t enforce model binding attributes.
It is a best practice to not include any body data in a GET request, you could obviously.
Completing the method, will do the same as the console project:
var bookReviews = new List<BookReviewResult>();
var books = BooksAbstraction.GetBooks(googleApiKey, request.Topic);
foreach (var book in books)
{
bookReviews.Add(new BookReviewResult
{
Book = book,
Videos = VideosAbstraction.GetVideos(googleApiKey, book.Title)
});
}
return Ok(new
{
BookReviews = bookReviews
});
Done.
Now will test the API, let’s run:
dotnet run
Now I will use Postman to test it. I usually test with no input first then with nulls and all the way to the happy path with correct input.
The frontend:
Will go back to the root solution directory. Will create a new React project, to learn more about React, visit: https://reactjs.org/docs/getting-started.html
npx create-react-app frontend
Wait for it to finish. Note you can’t name React projects with any capital letters due to NPM. So I will rename the directory after finishing to BookReview.Frontend. Move into it:
cd ./BookReview.Frontend
And run the app to check everything working:
yarn start
Should open a browser and show a default React app.
All fine and dandy, will start removing unnecessary files, cleaning up tests, empty CSS. I will leave the styling to you the programmer this time around.
Will create an api directory in the src. Will create a file named api.js and add:
const getBooksReviews = async (topic) => {
const response = await fetch(`http://localhost:5000/Book/Reviews?Topic=${topic}`, {
headers: {
"Content-Type": "application/json"
}
});
if (response.status === 200){
const bookReviews = (await response.json()).bookReviews;
return bookReviews;
}
return [];
};
module.exports.getBooksReviews = getBooksReviews;
This is an abstraction over calling the backend. I am also returning nothing in case of 200 response. Note that I have hardcoded the URL of the backend, this is an anti-pattern, you can add it into the environment variables using your production CI/CD, here it is just development, will hardcode it.
Here is where you appreciate the Cors setup we set up above in the backend, this React app server will most likely run on a different domain in testing and maybe production. A different domain is one where it has a different port, different subdomain or a different domain altogether.
Now going to App.js and making it so:
import './App.css';
import {React, useState} from "react";
import Search from "./components/Search";
import BookReviews from "./components/BookReviews";
function App() {
const [bookReviews, setBookReviews] = useState([]);
return (
<div className="App">
<header className="App-header">
<Search update={setBookReviews} />
</header>
<main>
<BookReviews data={bookReviews} />
</main>
</div>
);
}
export default App;
I decided to keep state regarding API response at this level so it can be shared downwards. I am having other components Search and BookReviews which you should create and will come to them soon. Note the pattern of passing the setBookReviews function to update as a prop to let the Search component update the data is a good practice. Note in this case am allowing for separation of concerns where the Search component is expected to call update from within without knowing anything about the parent. Also, the prop data={bookReviews} in BookReviews component will be updated with new props whenever the setBookReviews function gets called. React will take care of all of that.
Visiting the Search component:
import {React, useState} from "react";
import {getBooksReviews} from "../api/api";
function Search(props){
const [topic, setTopic] = useState("");
const onSubmit = (event) => {
event.preventDefault();
getBooksReviews(topic)
.then((data) => {
props.update(data);
});
};
return (
<form onSubmit={onSubmit} method="GET">
<input type="text" id="topic" required minLength="1" value={topic} onChange={(e) => setTopic(e.target.value)} />
<button type="submit">Get</button>
</form>
);
}
export default Search;
Here I am resetting the topic state every time the input (search box) changes value. When the form is submit, will do the classic AJAX way of preventing page submit to React server but will handle it on our own. Will call the API abstraction and when it is done will call the update prop function with the received data to update the state.
Note that I am not updating any UI indicator to say that I am searching and waiting for response. I encourage you to do it with other components.
Now will create the beast of the show the BookReview component, note there is plenty of if/else conditions ahead:
import {React} from "react";
function BookReview({bookReview}){
const subtitle = bookReview.book.subtitle ? (<li>Subtitle: {bookReview.book.subtitle}</li>) : (<></>);
const authors = bookReview.book.authors.length > 0
? (<li>Authors: <ul>{bookReview.book.authors.map((author) => <li key={author}>{author}</li>)}</ul></li>)
: (<></>);
return (
<li>
<ul>
<li>Book: {bookReview.book.title}</li>
{subtitle}
<li>Description <p>{bookReview.book.description}</p></li>
{authors}
<li>Since: {bookReview.book.published}</li>
<li>
Reviews:
<ul>
{bookReview.videos.map((video) => <li key={video.url}><a href={video.url} target="_blank" rel="noreferrer">
{video.title}
</a></li>)}
</ul>
</li>
</ul>
</li>
);
}
export default BookReview;
All there is just classic React. I have not displayed all the data, i.e. the price, page count and others.
And now onto the BookReviews component that wraps the BookReview component. You can have it all together as one component rather than two, either way works:
import {React} from "react";
import BookReview from "./BookReview";
function BookReviews({data}){
return (
<ul>
{data.length > 0
? data.map(bookReview => <BookReview bookReview={bookReview} key={`${bookReview.book.title}${bookReview.book.published}`} />)
: <span>No content, please search for something</span>}
</ul>
);
}
export default BookReviews;
The only exceptional thing I am doing here is checking if the array of book reviews is currently empty to just output No content, please search for something. As said earlier, I don’t have a mechanism of saying if the current state of the App is waiting for backend response or currently is the response.
That’s it for the UI. Now will run and wait for a response. Make sure the backend is running and have provided the correct Url for the API abstraction to consume.
Remarks:
I only implemented the barebone features. No styling whatsoever. You could style it and add embeddings of Youtube videos rather than just links.
This is an amazing abstraction anyone could build and extend. In fact you could make a compilation of many books on topics. Each API call costs rate limits by Google, keep that in mind when you over use the APIs.
Conclusion:
From plain text to UI. Programming goes a long way.
Thanks for reading this.