𧬠What is the DNA Stack?
Introduction:
First of all, let me give credit to Brandon Roberts for coming up with the name π₯Έ. The DNA stack represents a powerful combination of technologies: .NET Core, NX tools, and AnalogJS, providing developers with a cohesive and efficient environment to build robust applications. Let's briefly dive into each of these technologies without too much explanation, since they all have excellent documentation. Here, weβre going to focus on building something simple with this stack.
Note: A couple of assumptions before we start: you should have the .NET SDK installed already (I used version 8) and
dotnet-ef
. If you don't have these tools, please install them first: dotnet SDK and dotnet-ef.
π₯οΈ .NET Core: The Backbone
The Foundation of the Stack:
.NET Core serves as the backbone of the DNA stack, offering a cross-platform, open-source framework for building a wide range of applications, from web to cloud to IoT. Its performance, scalability, and extensive library support make it an ideal choice for backend development.
π οΈ NX Tools: Streamlining Development
The Power of Monorepos:
NX brings monorepo capabilities to the DNA stack, allowing developers to manage their projects more efficiently. With NX, you can easily share code, configure build processes, and ensure consistent standards across multiple projects. Itβs particularly valuable in large teams and projects, where maintaining structure and consistency is crucial.
π AnalogJS: The Angular Metaframework
Modernizing Angular Development:
Analog is a fullstack meta-framework designed for building applications and websites with Angular. It offers a similar experience to other popular meta-frameworks like Next.js, Nuxt, SvelteKit, and Qwik City, but with the powerful foundation of Angular.
Features:
- Supports Vite/Vitest/Playwright for efficient development and testing workflows.
- Integrates server and deployment capabilities powered by Nitro.
- Offers file-based routing for easy and intuitive route management.
- Enables server-side data fetching to enhance application performance.
- Allows the use of Markdown as content routes, making it easier to manage static content.
- Provides API/server routes for backend functionality within your Angular app.
- Supports both SSR (Server-Side Rendering) and SSG (Static Site Generation) for hybrid applications.
- Compatible with Angular CLI/Nx workspaces for streamlined project management.
- Integrates with Astro to use Angular components, expanding the flexibility of your development process.
With that said, let's dive into what we're going to build. In Analog, there's an example Notes generator that you can scaffold when you choose the tRPC option. However, we're not going to use tRPC today; instead, we're using Server Routes.
π The Notes App
Our full-stack app is super simple: you have Notes that you want to save (and delete as needed), so we're going to use the template inside the tRPC example and update it to use our new .NET Core backend. Letβs start by setting up the project and installing the dependencies.
npx create-nx-workspace@latest --preset=apps --ci=skip --name=notes-dna
This will generate our Nx workspace and create the folder for us to work. After it finishes, go to the folder and create an apps folder.
cd notes-dna
mkdir apps
Now we need to add two presets: one will be for our AnalogJS app and the other for our .NET Core app.
npm i -D @analogjs/platform
npm i -D @nx-dotnet/core
Once our two plugins are installed, itβs time to generate our Notes App π.
This will generate our Analog app:
nx g @analogjs/platform:app --analogAppName=notes --addTailwind=true --addTRPC=true
This command will initiate our workspace for .NET usage:
nx g @nx-dotnet/core:init
This command will generate our API application. We will need to choose webapi from the menu.
nx g @nx-dotnet/core:app api --language C# --args=-controllers --testTemplate=none --pathScheme=nx --skipSwaggerLib
If everything went well, the folder structure should look like this:
ββ Directory.Build.props
βββ Directory.Build.targets
βββ README.md
βββ apps
βΒ Β βββ api
βΒ Β βΒ Β βββ Controllers
βΒ Β βΒ Β βΒ Β βββ WeatherForecastController.cs
βΒ Β βΒ Β βββ NotesDna.Api.csproj
βΒ Β βΒ Β βββ NotesDna.Api.http
βΒ Β βΒ Β βββ Program.cs
βΒ Β βΒ Β βββ Properties
βΒ Β βΒ Β βΒ Β βββ launchSettings.json
βΒ Β βΒ Β βββ WeatherForecast.cs
βΒ Β βΒ Β βββ appsettings.Development.json
βΒ Β βΒ Β βββ appsettings.json
βΒ Β βΒ Β βββ obj
βΒ Β βΒ Β βΒ Β βββ NotesDna.Api.csproj.nuget.dgspec.json
βΒ Β βΒ Β βΒ Β βββ NotesDna.Api.csproj.nuget.g.props
βΒ Β βΒ Β βΒ Β βββ NotesDna.Api.csproj.nuget.g.targets
βΒ Β βΒ Β βΒ Β βββ project.assets.json
βΒ Β βΒ Β βΒ Β βββ project.nuget.cache
βΒ Β βΒ Β βββ project.json
βΒ Β βββ notes
βΒ Β βββ index.html
βΒ Β βββ package.json
βΒ Β βββ postcss.config.cjs
βΒ Β βββ project.json
βΒ Β βββ src
βΒ Β βΒ Β βββ app
βΒ Β βΒ Β βΒ Β βββ app.component.spec.ts
βΒ Β βΒ Β βΒ Β βββ app.component.ts
βΒ Β βΒ Β βΒ Β βββ app.config.server.ts
βΒ Β βΒ Β βΒ Β βββ app.config.ts
βΒ Β βΒ Β βΒ Β βββ pages
βΒ Β βΒ Β βΒ Β βββ (home).page.ts
βΒ Β βΒ Β βΒ Β βββ analog-welcome.component.ts
βΒ Β βΒ Β βββ main.server.ts
βΒ Β βΒ Β βββ main.ts
βΒ Β βΒ Β βββ note.ts
βΒ Β βΒ Β βββ styles.css
βΒ Β βΒ Β βββ test-setup.ts
βΒ Β βΒ Β βββ trpc-client.ts
βΒ Β βΒ Β βββ vite-env.d.ts
βΒ Β βββ tailwind.config.cjs
βΒ Β βββ tsconfig.app.json
βΒ Β βββ tsconfig.editor.json
βΒ Β βββ tsconfig.json
βΒ Β βββ tsconfig.spec.json
βΒ Β βββ vite.config.ts
βββ notes-dna.generated.sln
βββ nx.json
βββ package-lock.json
βββ package.json
βββ tsconfig.base.json
Remeber weβre here to build this small app, so take a good look into the documentation of .NET Core and AnalogJS for you to know what to expect with those folder structures.
Now check if everything runs, to run the api we use nx serve api
and to run the AnalogJs we use nx serve notes
.
By following the two localhost addresses of each application you should get something like this:
If you're still here and there are no errors then let's get started with our API:
𧩠The D on the DNA
First let's use Nugget to install a couple of dependencies Microsoft.EntityFrameworkCore.Design and Microsoft.EntityFrameworkCore.Sqlite
Note: Add the versions that belong to the dotnet core version you're using, ex: ver 8 if you're using dotnet 8.
First let's create our Entity since we're using EF that will help us to scaffold our database using the code first approach.
Create a folder at the root of the api project called Entity and create a class inside of that folder, call it Note.cs and add this code in there:
using System;
namespace NotesDna.Api.Entities;
public class Note
{
public int Id { get; set; }
public string Name { get; set; }
public string CreatedAt { get; set; }
}
This code is super simple, we're just adding the main object that will handle our Notes.
Then since we're using a Database we will need a DbContext so let's create that, create a Data folder at the same level of the Entities folder and create a class inside that one, call it DataContext.cs and add this code there:
using System;
using NotesDna.Api.Entities;
using Microsoft.EntityFrameworkCore;
namespace NotesDna.Api.Data;
public class DataContext(DbContextOptions options) : DbContext(options)
{
public DbSet<Note> Notes { get; set; }
}
Then let's modify our Program.cs class and get rid of what we won't need (it's just Swagger stuff) and leave it like this:
using NotesDna.Api.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddDbContext<DataContext>(opt =>
{
opt.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"));
});
var app = builder.Build();
// Configure the HTTP request pipeline.
app.MapControllers();
app.Run();
And now let's add our connection to the app properties file appsettings.Development.json leave the file like this:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Data source=notes.db"
}
}
Now let's do the fun part, add the controller that pretty much will provide the CRUD operations for us, inside the Controllers folder add a class and call it NotesController.cs and add this logic to it:
using NotesDna.Api.Data;
using NotesDna.Api.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace NotesDna.Api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class NotesController(DataContext context) : ControllerBase
{
private readonly DataContext _context = context;
[HttpGet]
public async Task<ActionResult<IEnumerable<Note>>> GetNotes()
{
try
{
var notes = await _context.Notes.ToListAsync();
return Ok(notes);
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error getting the notes");
}
}
[HttpGet("{id:int}")]
public async Task<ActionResult<Note>> GetNote(int id)
{
try
{
var note = await _context.Notes.FindAsync(id);
if (note == null) return NotFound();
return Ok(note);
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error getting the note record");
}
}
[HttpPost]
public async Task<ActionResult<Note>> AddNote([FromBody] Note note)
{
try
{
if (note == null)
return BadRequest();
await _context.Notes.AddAsync(note);
await _context.SaveChangesAsync(); // Ensure the Id is generated
return CreatedAtAction(nameof(GetNote), new { id = note.Id }, note);
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error creating new note record");
}
}
[HttpDelete("{id:int}")]
public async Task<ActionResult> DeleteNote(int id)
{
try
{
var note = await _context.Notes.FirstOrDefaultAsync(x => x.Id == id);
if (note == null) return NotFound();
_context.Notes.Remove(note);
await _context.SaveChangesAsync();
return Ok();
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, "Error deleting the note record");
}
}
}
}
Here's a brief explanation of each method in the NotesController
even when they seek a little self explanatory π₯Έ:
GetNotes
Method
[HttpGet]
public async Task<ActionResult<IEnumerable<Note>>> GetNotes()
{
var notes = await _context.Notes.ToListAsync();
return Ok(notes);
}
- Purpose: Retrieves all notes from the database.
-
Logic: It asynchronously fetches the list of notes and returns them in an
Ok
response, indicating a successful operation.
GetNote
Method
[HttpGet("{id:int}")]
public async Task<ActionResult<Note>> GetNote(int id)
{
var note = await _context.Notes.FindAsync(id);
if (note == null) return NotFound();
return Ok(note);
}
- Purpose: Retrieves a specific note by its ID.
-
Logic: It asynchronously finds the note by ID. If found, it returns the note in an
Ok
response; if not, it returns aNotFound
response.
AddNote
Method
[HttpPost]
public async Task<ActionResult<Note>> AddNote([FromBody] Note note)
{
await _context.Notes.AddAsync(note);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetNote), new { id = note.Id }, note);
}
- Purpose: Adds a new note to the database.
-
Logic: It asynchronously adds the note to the database and saves the changes. Afterward, it returns a
CreatedAtAction
response, pointing to the newly created note.
DeleteNote
Method
[HttpDelete("{id:int}")]
public async Task<ActionResult> DeleteNote(int id)
{
var note = await _context.Notes.FirstOrDefaultAsync(x => x.Id == id);
if (note == null) return NotFound();
_context.Notes.Remove(note);
await _context.SaveChangesAsync();
return Ok();
}
- Purpose: Deletes a note by its ID.
-
Logic: It asynchronously searches for the note by ID. If found, the note is removed, and the changes are saved. It returns an
Ok
response for a successful deletion orNotFound
if the note doesn't exist.
And i almost forget let's clean a little bit our launchSettings.json
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:20303",
"sslPort": 44331
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5000;https://localhost:5001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
This should allows us to run the server at port 5000 for http and 5001 for https.
now let's generate our DB and add some data to test.
On the root of the NX workspace use your terminal to run this:
dotnet ef migrations add InitialMigration -o Data/Migrations --msbuildprojectextensionspath dist/intermediates/apps/api/obj --project apps/api/NotesDna.Api.csproj
This should create a migration inside your Data folder that will pretty much scaffold our database, now we need to apply this to our database using:
dotnet ef database update --msbuildprojectextensionspath dist/intermediates/apps/api/obj --project apps/api/NotesDna.Api.csproj
So now let's run it and check if everything is working properly.
Note: After our changes we should not see the swagger URL anymore so we can consume the API manually in the browser or using Postman.
Most likely you won't see anything, because we don't have any data, use your favorite DB tool to add a couple of records and then try again.
If everything wen't well then we will continue with the AnalogJs part.
π Conclusion: Building the Foundation
In this first part of our journey with the DNA stack, we've successfully laid the groundwork by setting up our backend with .NET Core and integrating it with NX and AnalogJS. We created a basic Notes application, configured the necessary tools, and established a solid structure for further development. This stack offers a powerful and flexible environment for building fullstack applications, and we're just getting started.
In the upcoming part, we'll focus on completing the AnalogJS application, building out the front-end features, and fully integrating it with our backend. Stay tuned as we continue to enhance our project and unlock the full potential of the DNA stack.
If you found this article helpful, feel free to connect with me on Twitter, Threads, or LinkedIn. Let's continue this journey together! π»ππ
If you'd like to support my work, consider buying me a coffee. Your support is greatly appreciated! βοΈ
Top comments (0)