In this article, you will learn how to create an ASP.NET Core Web API project that also includes rendering files that the browser requests. There are a few nuances to this!
Table of Contents
In my recent articles, Adaptive Hierarchical Knowledge Management, Part I and Part II (ok, shameless self-reference), I had created an ASP.NET Core Web API project rather than an ASP.NET Core Web App project, because I was implementing an API, not a web application. None-the-less, my API includes default pages for administration and testing of the API functions, and to render those pages and the .js files, I ended up implementing a controller to return the .html and .js files, which is very much the wrong approach.
In this short article, I walk you through how to create an ASP.NET Core Web API project that also includes rendering files that the browser requests. There are a few nuances to this!
Why Not Use Swagger?
Swagger is great for generating a UI for testing endpoints. The point of this article is that it illustrates how to add web pages for situations where you want something that is a) more user-friendly and b) more complicated, like an admin page, that interacts with multiple endpoints.
Why Not Use a ASP.NET Core Web App Then?
Because there are times you don't need Razor project (one option in VS2019) nor do you want a Model-View-Controller project (another option in VS2019) and you certainly don't need Blazor (yet another option in VS2019.) You just want an API with some built-in pages that does more than Swagger but, because it's an API you're writing, doesn't require Razor, Blazor, or MVC. (Personally, I don't think a web application, regardless of the complexity, should ever "require" using one of those three options, but that's me.)
So you've created a Web API project:
and you have your spiffy controller:
[ApiController]
[Route("[controller]")]
public class SpiffyController : ControllerBase
{
[HttpGet]
public object Get()
{
return Content("<b>Hello World</b>", "text/html");
}
}
Then, following this excellent StackOverflow post:
You add these two lines to Startup.cs in the Configure
method:
app.UseDefaultFiles();
app.UseStaticFiles();
UseDefaultFiles
: Setting a default page provides visitors a starting point on a site. To serve a default page from wwwroot without a fully qualified URI, call the UseDefaultFiles method -- Serve default documents UseStaticFiles
: Static files are stored within the project's web root directory. The default directory is {content root}/wwwroot -- Static files in ASP.NET Core
Create the folder wwwroot and put index.html in it (and whatever else you want at the top level):
My index.html file looks like this for this article:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TypeScript HTML App</title>
</head>
<body>
<div>
<button id="btnHi">Say hello</button>
<div id="response"></div>
<button id="btnGoodbye">Goodbye</button>
</div>
</body>
</html>
and goodbye.html looks like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<h2>Goodbye!</h2>
</body>
</html>
At this point, running the project, we see:
and:
So cool, we have rendering of the index.html.
Then you create a Scripts folder and add a TypeScript file (note that I put the folder directly under the project):
which gives you this message:
So you do that, and you also set the ECMAScript version to at least 2017:
I don't want my .js files to go into the same Scripts folder, I want them only under wwwroot/js:
So I go to the project properties and redirect the Typescript compiler output:
and I see that that worked:
But index.html doesn't know how to load the .js file that will be compiled from the TypeScript file. So we add this line in index.html:
<script src="/js/app.js"></script>
Now, my demo script file looks like this:
window.onload = () => {
new App().init();
};
class App {
public init() {
document.getElementById("btnHi").onclick = () => this.Hi();
document.getElementById("btnGoodbye").onclick = () => window.open('goodbye.html', '_self');
}
private Hi(): void {
XhrService.get(`${window.location.origin}/Spiffy`)
.then(xhr => {
document.getElementById("response").innerHTML = xhr.responseText;
});
}
}
class XhrService {
public static async get(url: string, ): Promise<XMLHttpRequest> {
const xhr = new XMLHttpRequest();
return new Promise((resolve, reject) => {
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr);
} else {
reject(xhr);
}
}
};
xhr.open("GET", url, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send();
});
}
}
And now clicking on the buttons, we see:
and:
Developers describe RequireJS as "JavaScript file and module loader". RequireJS loads plain JavaScript files as well as more defined modules. It is optimized for in-browser use, including in a Web Worker, but it can be used in other JavaScript environments, like Rhino and Node. It implements the Asynchronous Module API. Using a modular script loader like RequireJS will improve the speed and quality of your code. On the other hand, Webpack is detailed as "A bundler for javascript and friends". -- require vs. webpack
The above is a simple example, but what if my TypeScript classes are in separate files? I tend to prefer using require
rather than a bundler like Webpack, if for no other reason than it is easier to configure (in my opinion) and I'm used to it.
DO NOT DO THIS! You'll get all sorts of stuff you don't need. Do this instead:
npm install --save @types/requirejs
which installs just the d.ts file for require
:
Create an AppConfig.ts file in the Scripts folder:
import { App } from "./App"
require(['App'],
() => {
const appMain = new App();
appMain.run();
}
);
and refactor app.ts into two files:
App.ts
import { XhrService } from "./XhrService";
export class App {
public run() {
document.getElementById("btnHi").onclick = () => this.Hi();
document.getElementById("btnGoodbye").onclick = () => window.open('goodbye.html', '_self');
}
private Hi(): void {
XhrService.get(`${window.location.origin}/Spiffy`)
.then(xhr => {
document.getElementById("response").innerHTML = xhr.responseText;
});
}
}
XhrService.ts
export class XhrService {
public static async get(url: string,): Promise<XMLHttpRequest> {
const xhr = new XMLHttpRequest();
return new Promise((resolve, reject) => {
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr);
} else {
reject(xhr);
}
}
};
xhr.open("GET", url, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send();
});
}
}
Run the Web API project, either with the project Debug
property option for Launch Browser as the controller name:
which will hit the API endpoint, or with nothing:
Which will render index.html with the functionality implemented in App.ts.
The full project directory now looks like this:
The TypeScript files can be debugged in Visual Studio:
Unfortunately, because the TypeScript .ts files are in the Scripts folder, not the wwwrooot/js folder, Chrome, when you try to set a breakpoint, displays this error:
We can fix this by adding this line in Startup.cs:
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(env.ContentRootPath, "Scripts")),
RequestPath = "/Scripts"
});
Now Chrome can find the .ts files and you can debug in the Chrome console:
We now have a working example of:
- Adding TypeScript to an ASP.NET Core Web App project.
- Adding
require
so we can reference TypeScript files and do so without using a bundler. - Serve the default files, like index.js
- Separate the TypeScript .ts files from the compiled .js files.
- Help Chrome's debugger find the .ts files.
And now, I can go fix my gloriosky project mentioned in the intro!
- 10th April, 2021: Initial version