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:
![Image 1](/KB/Articles/5299530/createProject.png)
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):
![Image 2](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
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:
![Image 3](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
and:
![Image 4](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
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):
![Image 5](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
which gives you this message:
![Image 6](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
So you do that, and you also set the ECMAScript version to at least 2017:
![Image 7](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
I don't want my .js files to go into the same Scripts folder, I want them only under wwwroot/js:
![Image 8](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
So I go to the project properties and redirect the Typescript compiler output:
![Image 9](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
and I see that that worked:
![Image 10](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
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:
![Image 11](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
and:
![Image 12](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
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.
![Image 13](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
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
:
![Image 14](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
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:
![Image 15](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
which will hit the API endpoint, or with nothing:
![Image 16](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
Which will render index.html with the functionality implemented in App.ts.
The full project directory now looks like this:
![Image 17](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
The TypeScript files can be debugged in Visual Studio:
![Image 18](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
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:
![Image 19](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
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:
![Image 20](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
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