DEV Community

Cover image for The Dotnet, Nx, AnalogJs (Angular) Stack is here - Part 1
Luis Castro
Luis Castro

Posted on

The Dotnet, Nx, AnalogJs (Angular) Stack is here - Part 1

🧬 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
npm i -D @nx-dotnet/core
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This command will initiate our workspace for .NET usage:

nx g @nx-dotnet/core:init
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

Analog application NET Core application

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; }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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");
      }
    }

  }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
  • 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);
}
Enter fullscreen mode Exit fullscreen mode
  • 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 a NotFound 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);
}
Enter fullscreen mode Exit fullscreen mode
  • 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();
}
Enter fullscreen mode Exit fullscreen mode
  • 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 or NotFound 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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)