Should one host SPA independently from API?
If you need to scale the front-end independently from the back-end then go with a Content Delivery Network (CDN) that lets you use own domain name, has configurable caching policies and all other modern attributes like 100% SLA, etc.
Your concerns might be
- Desynchronised deployment of the two essential parts of a single solution.
- Extra resources (human-hours) to maintain the distributed infrastructure (another point of failure);
- Trade-offs for delivering essential parameters to the front-end. Either
- you make the front-end bundle mutable between staging environments and include the parameters or
- you keep it immutable and need extra time on the start-up to fetch them.
I'd dismiss concerns regarding additional OPTIONS requests for CORS as HTTP/2 brings latency down to 0.
Hosting SPA from WebAPI wouldn't have those concerns (but doesn't allow independent scaling).
How to host SPA from .NET WebAPI?
When it comes to hosting a SPA from a .NET API, there are three extension methods that do all the heavy lifting (starting from .NET Core 2.x and it's still the case in .NET 5):
- UseSpa serves the default page (
index.html
) and redirects all requests there.
- UseStaticFiles serves other than
index.html
static files under the web root folder (wwwroot
, by default). Without this method Kestrel would return index.html in response on all requests for static content.
- UseSpaStaticFiles does a similar thing but it requires ISpaStaticFileProvider to be registered to resolve location of the static files. You need it if the static files are NOT under the default web root folder
wwwroot
.
To host SPA from wwwroot
you need just UseSpa
and UseStaticFiles
methods.
Don't forget 2 important things:
- caching policies for static assets;
- injecting environment variables on serving the front-end.
So your Configure
method in the Startup.cs
may have the following additions:
// Serves other than 'index.html' files from wwwroot folder with a cache policy
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
var headers = ctx.Context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue { MaxAge = TimeSpan.FromDays(12*30) };
}
});
// Serves 'index.html' with no-cache policy and passing variables in a cookie
app.UseSpa(c => c.Options.DefaultPageStaticFileOptions = new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
var response = ctx.Context.Response;
response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue
{
NoCache = true,
NoStore = true,
MustRevalidate = true,
MaxAge = TimeSpan.Zero
};
// Passing a variable to SPA in a short-lived cookie
response.Cookies.Append("importantVariable", "Value", new CookieOptions { MaxAge = TimeSpan.FromSeconds(30) });
}
})
See more in this blog post that also considers other options. No problems modifying the index.html
& static files for injecting variables if you don't favour cookies.
Also check out this open source project at GitHub that does the trick with hosting SPA and passing parameters in cookies (direct link to Startup configuration and reading cookies in Angular).