Skip to content

Angular Libraries with Nx for Enterprise Apps

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

nx-library-enterprise

What is Nx?

Nx is an extensible dev tool for monorepos.

"Using Nx, you can add TypeScript, Cypress, Jest, Prettier, Angular, React, Next.js, and Nest into your dev workflow. Nx sets up these tools and allows you to use them seamlessly. Nx fully integrates with the other modern tools you already use and love." - Nx Team.

Why Use Angular Libraries?

You should use Angular libraries because sharing code across one app is easy, but sharing code between different projects requires extra steps.

When we do encounter services or components that can be reused across different teams and projects, and that ideally do not change very often, we may want to build an Angular Library.

Downside Of Using Libraries?

  • You have to link your library to your main project, and rebuild it on every change.
  • You will need to keep syncing your project with the latest version of the library.

Advatanges Of Using Libraries?

  • We need to think and build these modules with reusability in mind.
  • Publish and share these libraries with other teams or projects.

What are Monorepos?

Monorepos are a source control pattern, in which essentially all of the codebase lives in the same repository. All projects will always use the latest version of the code. That's one of the reasons why Nx comes in handy when working with libraries.

Advatanges Of Using Angular Monorepos?

  • Same library version for every app.
  • Ease of maintenance: when you update a shared library, you update it for all apps.
  • No conflicts between versions.

For more information on Angular Monorepos

Let's Get Our Hands Dirty

  1. Run the following command in your terminal to install Nx globally.
npm install -g @nrwl/schematics
  1. Create a Nx Workspace. When asked about 'preset', select empty.
npx create-nx-workspace@latest thisdot
carbon

When asked what CLI should power your Nx workspace - select Angular CLi

carbon (1)

Nx Workspace Structure

Screen Shot 2019-11-20 at 5.32.56 PM
  1. Add the capability to create Angular applications via:
ng add @nrwl/angular --defaults
  1. Create a new angular app inside of your Nx workspace.
ng g @nrwl/angular:application employees

Then it will ask you which stylesheet format would you like to use. Select sass. carbon (2)

press enter

The next question will be, "Would you like to configure routing for this application? (y/N)" Type y

press enter

Project Structure

Screen Shot 2019-11-20 at 5.39.48 PM
  1. Serve the Angular app, and go to http://localhost:4200.
ng serve employees

You should see something like this: Screen Shot 2019-11-20 at 5.41.29 PM

For this app, we are going to create a library that contains an employee interface that will be shared across multiple applications.

  1. Create a sharable interface with the following command:
ng g @nrwl/workspace:lib employee
  1. Go to libs/employee/src/lib/employee.ts and "copy-paste" the following:
export interface Employee {
  id: number;
  name: string;
}
  1. Go to your app.component.ts file inside of your employees application.

Whenever you need to use the employee's interface inside of this workspace, you will import it to your file as following:

import { Employee } from '@thisdot/employee';

Note: If You are using vscode and doens't recognize it - restart vscode.

A cool thing about Nx is that, if you have your backend inside of this workspace, you can reuse this interface as well.

Creating A UI Library

  1. To create the ui library, run the following command:
ng g @nrwl/angular:lib ui

Your project structure will look like this:

Screen Shot 2019-11-20 at 5.50.06 PM
  1. Now go to your ui.module.ts. You file should look like this:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  imports: [CommonModule]
})
export class UiModule {}

Time To Create A Component In Our UI Library

  1. Run the following command:
ng g component employee-list --project=ui --export

Your project structure should look like this Screen Shot 2019-11-20 at 5.54.14 PM

  1. Now lets go to your employee-list.component.ts file insdie of our ui library.
  • You will add the employee interface we created.
  • You will create an input that takes an array of employees.
  • We will add a trackBy function just for you to see how we create one for optimization.

Your file should look like this:

import { Component, OnInit, Input } from '@angular/core';
import { Employee } from '@thisdot/employee';
@Component({
  selector: 'thisdot-employee-list',
  templateUrl: './employee-list.component.html',
  styleUrls: ['./employee-list.component.scss']
})
export class EmployeeListComponent implements OnInit {
  @Input() employees: Employee[];
  constructor() { }

  ngOnInit() {}

  trackById(employee: Employee) {
    return employee ? employee.id : null;
  }
}
  1. Inside of your employee.component.html file
<ul>
  <li *ngFor="let e of employees; trackBy: trackById(e)">{{ e.name }}</li>
</ul>

As you can see, I'm using the trackBy function to promote better performance of our app.

For more information on trackby visit this link.

Creating A Service

  1. Run the following command to create a service inside of our ui library:
ng g s employee --project=ui
  1. Now go to your ui library, and search for your employee.service file, and make sure it looks like the following:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Employee } from '@thisdot/employee';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class EmployeeService {
  employees$: Observable<Employee[]>;
  constructor(private http: HttpClient) {
    this.employees$ = this.http.get<Employee[]>(
      'https://my-json-server.typicode.com/devpato/nx-fake-data/employees'
    );
  }
}
  1. Now go to your index.ts file
Screen Shot 2019-11-20 at 6.04.06 PM
  1. Add the service to your file. Your file should look like this:
export * from './lib/ui.module';
export * from './lib/employee.service';
  1. Now go to your ui.module.ts. The file should look like this:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { EmployeeListComponent } from './employee-list/employee-list.component';

@NgModule({
  imports: [CommonModule, HttpClientModule],
  declarations: [EmployeeListComponent],
  exports: [EmployeeListComponent]
})
export class UiModule {}

Note: you can see I have added the HttpClientModule and Nx has added the component for me already.

Time To Use Our UI Library

  1. Go to your employees app, and open the app.module.ts
  • Inject our library at the top of the file
  • Then add it to your imports
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { RouterModule } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { UiModule } from '@thisdot/ui';

@NgModule({
  declarations: [AppComponent],
  imports: [
    UiModule,
    HttpClientModule,
    BrowserModule,
    RouterModule.forRoot([], { initialNavigation: 'enabled' })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Our Ui library is ready to be used in this project.

  1. Now, open your app.component.html file inside of your employees app, and copy paste the following code.
<div class="employees-container">
  <img src="../assets/images/logotdm.png" />
  <h1>Employees</h1>
  <thisdot-employee-list 
  [employees]="employeeService.employees$ | async">
  </thisdot-employee-list>
</div>
  • This is where I'm injecting the employee-list component we created.
  1. Open in your app.component.ts, and change it to match the example below:
import { Component } from '@angular/core';
import { EmployeeService } from '@thisdot/ui';

@Component({
  selector: 'thisdot-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  constructor(private employeeService: EmployeeService) {}
}

As you can see, I'm injecting the service we created inside of the ui library.

  1. Go to your app.component.scss file, and add the following code.
.employees-container {
  display: flex;
  width: 100%;
  height: 100vh;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  img {
    width: 200px;
  }
}

Bonus (Shared Assests)

Now, I'm Going to Show You How to Share Assets Between Projects.

  1. Go to your ui library, and create a subfolder called 'shared-assets', then create another folder called 'images', and add an image there. Then name it as shown on the picture.
Screen Shot 2019-11-20 at 6.29.59 PM
  1. Now go to your angular.json, and find assets.

Your file should look like this:

"assets": [
             "apps/employees/src/favicon.ico",
              "apps/employees/src/assets",
            {
              "glob": "**/*",
              "input": "./libs/ui/src/lib/shared-assets",
              "output": "./assets"
            }
          ]

Restart VS Code to make sure it detects all the changes.

Time To Test Our App

  1. In your command line run:
ng serve employees
Screen Shot 2019-11-20 at 6.33.40 PM

And We Are Done! :)

About Author:


Pato Software Engineer at This Dot | Auth0 Ambassador Twitter: devpato Stackoverflow: devpato Github: devpato AngularJax Meetup Founder

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Exploring Angular Forms: A New Alternative with Signals cover image

Exploring Angular Forms: A New Alternative with Signals

Exploring Angular Forms: A New Alternative with Signals In the world of Angular, forms are essential for user interaction, whether you're crafting a simple login page or a more complex user profile interface. Angular traditionally offers two primary approaches: template-driven forms and reactive forms. In my previous series on Angular Reactive Forms, I explored how to harness reactive forms' power to manage complex logic, create dynamic forms, and build custom form controls. A new tool for managing reactivity - signals - has been introduced in version 16 of Angular and has been the focus of Angular maintainers ever since, becoming stable with version 17. Signals allow you to handle state changes declaratively, offering an exciting alternative that combines the simplicity of template-driven forms with the robust reactivity of reactive forms. This article will examine how signals can add reactivity to both simple and complex forms in Angular. Recap: Angular Forms Approaches Before diving into the topic of enhancing template-driven forms with signals, let’s quickly recap Angular's traditional forms approaches: 1. Template-Driven Forms: Defined directly in the HTML template using directives like ngModel, these forms are easy to set up and are ideal for simple forms. However, they may not provide the fine-grained control required for more complex scenarios. Here's a minimal example of a template-driven form: ` ` 2. Reactive Forms: Managed programmatically in the component class using Angular's FormGroup, FormControl, and FormArray classes; reactive forms offer granular control over form state and validation. This approach is well-suited for complex forms, as my previous articles on Angular Reactive Forms discussed. And here's a minimal example of a reactive form: ` ` Introducing Signals as a New Way to Handle Form Reactivity With the release of Angular 16, signals have emerged as a new way to manage reactivity. Signals provide a declarative approach to state management, making your code more predictable and easier to understand. When applied to forms, signals can enhance the simplicity of template-driven forms while offering the reactivity and control typically associated with reactive forms. Let’s explore how signals can be used in both simple and complex form scenarios. Example 1: A Simple Template-Driven Form with Signals Consider a basic login form. Typically, this would be implemented using template-driven forms like this: ` ` This approach works well for simple forms, but by introducing signals, we can keep the simplicity while adding reactive capabilities: ` ` In this example, the form fields are defined as signals, allowing for reactive updates whenever the form state changes. The formValue signal provides a computed value that reflects the current state of the form. This approach offers a more declarative way to manage form state and reactivity, combining the simplicity of template-driven forms with the power of signals. You may be tempted to define the form directly as an object inside a signal. While such an approach may seem more concise, typing into the individual fields does not dispatch reactivity updates, which is usually a deal breaker. Here’s an example StackBlitz with a component suffering from such an issue: Therefore, if you'd like to react to changes in the form fields, it's better to define each field as a separate signal. By defining each form field as a separate signal, you ensure that changes to individual fields trigger reactivity updates correctly. Example 2: A Complex Form with Signals You may see little benefit in using signals for simple forms like the login form above, but they truly shine when handling more complex forms. Let's explore a more intricate scenario - a user profile form that includes fields like firstName, lastName, email, phoneNumbers, and address. The phoneNumbers field is dynamic, allowing users to add or remove phone numbers as needed. Here's how this form might be defined using signals: ` > Notice that the phoneNumbers field is defined as a signal of an array of signals. This structure allows us to track changes to individual phone numbers and update the form state reactively. The addPhoneNumber and removePhoneNumber methods update the phoneNumbers signal array, triggering reactivity updates in the form. ` > In the template, we use the phoneNumbers signal array to dynamically render the phone number input fields. The addPhoneNumber and removePhoneNumber methods allow users to reactively add or remove phone numbers, updating the form state. Notice the usage of the track function, which is necessary to ensure that the ngFor directive tracks changes to the phoneNumbers array correctly. Here's a StackBlitz demo of the complex form example for you to play around with: Validating Forms with Signals Validation is critical to any form, ensuring that user input meets the required criteria before submission. With signals, validation can be handled in a reactive and declarative manner. In the complex form example above, we've implemented a computed signal called formValid, which checks whether all fields meet specific validation criteria. The validation logic can easily be customized to accommodate different rules, such as checking for valid email formats or ensuring that all required fields are filled out. Using signals for validation allows you to create more maintainable and testable code, as the validation rules are clearly defined and react automatically to changes in form fields. It can even be abstracted into a separate utility to make it reusable across different forms. In the complex form example, the formValid signal ensures that all required fields are filled and validates the email and phone numbers format. This approach to validation is a bit simple and needs to be better connected to the actual form fields. While it will work for many use cases, in some cases, you might want to wait until explicit "signal forms" support is added to Angular. Tim Deschryver started implementing some abstractions around signal forms, including validation and wrote an article about it. Let's see if something like this will be added to Angular in the future. Why Use Signals in Angular Forms? The adoption of signals in Angular provides a powerful new way to manage form state and reactivity. Signals offer a flexible, declarative approach that can simplify complex form handling by combining the strengths of template-driven forms and reactive forms. Here are some key benefits of using signals in Angular forms: 1. Declarative State Management: Signals allow you to define form fields and computed values declaratively, making your code more predictable and easier to understand. 2. Reactivity: Signals provide reactive updates to form fields, ensuring that changes to the form state trigger reactivity updates automatically. 3. Granular Control: Signals allow you to define form fields at a granular level, enabling fine-grained control over form state and validation. 4. Dynamic Forms: Signals can be used to create dynamic forms with fields that can be added or removed dynamically, providing a flexible way to handle complex form scenarios. 5. Simplicity: Signals can offer a simpler, more concise way to manage form states than traditional reactive forms, making building and maintaining complex forms easier. Conclusion In my previous articles, we explored the powerful features of Angular reactive forms, from dynamic form construction to custom form controls. With the introduction of signals, Angular developers have a new tool that merges the simplicity of template-driven forms with the reactivity of reactive forms. While many use cases warrant Reactive Forms, signals provide a fresh, powerful alternative for managing form state in Angular applications requiring a more straightforward, declarative approach. As Angular continues to evolve, experimenting with these new features will help you build more maintainable, performant applications. Happy coding!...

Using HttpClient in Modern Angular Applications cover image

Using HttpClient in Modern Angular Applications

Introduction With all the wonderful treats that the Angular team has given us during the recent "renaissance" era, many new developers are joining in on the fun. And one of the challenges they'll face at some point is how to call an API from your Angular application properly. Unfortunately, while searching for a guide on how to do this, they might stumble upon a lot of outdated information. Hence, this article should serve as a reliable guide on how to use the HttpClient in Angular >= 17. The Setup To make an HTTP request in Angular, you can take advantage of the HttpClient provided by the @angular/common/http package. To use it, you'll need to provide it. Here's how you can do that for the whole application using the bootstrapApplication function from @angular/platform-browser: ` With that, you should be good to go. You can now inject the HttpClient into any service or component in your application. Using HttpClient in Services Let's take a common example: You have a database object, say a Movie, and you want to implement CRUD operations on it. Typically, you'll want to create a service that provides methods for these operations. Let's call this service MovieService and create a skeleton for it with a method for getting all movies. ` Implementing the Method using HttpClient Let's assume we have a GraphQL API for our movies. We can implement our getAllMovies using HttpClient to make a request to fetch all movies. First, we will need to define a new type to represent the response from the API. This is especially important when you are using GraphQL, which may return a specific structure, such as: ` When working with a real API, you'll likely use some code generator to generate the types for the response from the GraphQL schema. But for the sake of this example, we'll create an interface to represent the response manually: ` Now, we can implement the getAllMovies method using HttpClient: ` > Note: The post method is used here because we are sending a request body. If you are making a GET request (e.g. to a REST API), you can use the get method instead. The getAllMovies method returns an Observable of MoviesListResponse. In this example, I have typed it explicitly to make it obvious at first glance, but you could also omit the type annotation, and TypeScript should infer it. > Note: While I'm excited about signals as much as the next guy, making HTTP requests is one of the typical async operations for which RxJS Observables are a perfect fit, making a great argument for RxJS still having a solid place in Angular alongside signals. Using the Service in a Component Now that we have our MovieService set up, we can use it as a component to fetch and display all movies. But first, let's create a Movie interface to represent the structure of a movie. Trust me, this will prevent many potential headaches down the line. Although using any at the beginning and implementing the types later is a valid approach in some cases, using data fetched from an API without validating the type will inevitably lead to bugs that are difficult to solve. ` Now, we can start implementing our standalone MoviesComponent: ` In this component, we are calling the getAllMovies method from the MovieService in the constructor to fetch all movies and assign them to the movies property which we will use to display the movies in the template. ` In this case, placing our code inside the constructor is safe because it doesn’t depend on any @Input(). If it were, our code would fail because the inputs aren’t initialized at time of instantiation. That's why it is sometimes recommended to place logic in ngOnInit instead. You could also put this call in an arbitrary method that is called e.g. on a button click. Another way to handle the subscription is to use the async pipe in the template. This way, Angular will automatically subscribe and unsubscribe from the observable for you and you won't have to assign the response to a property in the component. Due to the structure of the returned data, however, we'll need to use the RxJS map operator to extract the movies from the response. ` > Note using the pipe method to chain the map operator to the observable returned by getAllMovies. This is a common pattern when working with RxJS observables introduced in RxJs 5.5. If you see code that uses the map operator directly on the observable and wonder why it isn't working for you, it's likely using an older version of RxJS. Now, we can simply apply the async pipe in the template to subscribe to the observable and display the movies: ` Conclusion HttpClient in Angular is pretty straightforward, but with all the changes to RxJS and Angular in the past few years, it can pose a significant challenge if you're not super-experienced with those technologies and stumble upon outdated resources. Following this article, you should hopefully be able to implement your service using HttpClient to make requests to an API in your modern Angular application and avoid the struggle of copying outdated code snippets....

PWA Push Notifications with Firebase (Cloud Messaging)-pt1 cover image

PWA Push Notifications with Firebase (Cloud Messaging)-pt1

Push Notification In Your PWA Have you ever wondered how to add the famous/annoying push notifications to your app? Well, in this tutorial I'm going to show you how to do it using Firebase Cloud Messaging. *Note:* This tutorial requires some basic knowledge on PWAs and Service Workers. You can take a look to my Intro to PWA and Service Workers here and About PWA and notifications here. Before we begin, I need to clarify that the Notification API, and the Push API, are not the same. People get them confused all the time. Push API: The Push API gives web applications the ability to receive messages pushed to them from a server, whether or not the web app is in the foreground, or even currently loaded, on a user agent. This lets developers deliver asynchronous notifications and updates to users that opt in, resulting in better engagement with timely new content. Let's doooo it!! The final code is in the FINAL branch inside of the repo. 1) Clone this repo: https://github.com/devpato/pwa-FCM-notifications-tutorial As you can see, I already have the basic structure of the app created for you, since we are only going to worry about how to send the messages via push notifications using the Firebsae Cloud Messaging service. 2) Navigate to the index.html file. Notice I imported Firebase for you: ` 3) Navigate to Firebase.com, and create an account if you don't have one. 4) Once you are in the Firebase console, navigate to project settings (in case you don't have a project yet - just create it there) 5) Inside of project setting, under the General tab scroll all the way down to find your Firebase SDK snippet (if it's not there yet - this means that you've created a new project, and need to add there an app. Either way, this can be done in the same place where you then will have your SDK snippet - under the General tab). Copy paste it in a safe place. The snippet should look like this: 6) Go to your index.js file, and copy paste the following after the global variables I declared for you. Replace it with your project's customized code - the snippet from step 4. ` 7) Right after - initialize the firebase instance. ` 8) Then, we are going to create a constant called messaging, and will set it to firebase messaging service. ` 9) Time to request permission from firebase cloud messaging. Once we get the thumbs up, they will give us a token as a promise. ` 10) Then, we are going to use the messaging.onMessage() method. This is used for receiving data, and notification payloads, by all users that are currently viewing the page (the page is in the foreground). To do so, we add the following code: ` 11) Notice a firebase-messaging-sw.js file. This file name is going to be searched by the Firebase SDK. The file needs to be in the ROOT of your project. The Firebase SDK will do some magic in the background to register the file as a service worker. 12) Inside of your firebase-messaging-sw.js, initialize the Firebase app by passing in the messagingSenderId. The sender id can be found inside of your project settings as the following image shows. ` 13) Retrieve an instance of Firebase Messaging so that it can handle background messages. ` 14) Background message handler (this one will be invoked when the page is in the backround) ` Test The Notification 15) Run the app using any http server 16) Inside of your Cloud Messaging settings (a tab in the Firebase Console > Project Settings), copy the server key. 17) If you have a Postman http client, do the following: POST URL:* https://fcm.googleapis.com/fcm/send * HEADERS: Content-Type - application/json Authorization - key=server_key BODY: ` Then, click the Send button. At this point, if our app is in the foreground (it is currently opened tab in your browser), then you'll see the message we've sent in the console - handled by messaging.onMessage. But if it is in the background, then it will be handled by messaging.setBackgroundMessageHandler in the service worker, and you'll see something like this: Test your app on a real device by deploying to Firebase or any other hosting provider. If you want to host your app on the Firebase - take a look at my other tutorial. In the next tutorials, I will show you how to successfully subscribe to notifications and push them using the Firebase console....

“We were seen as amplifiers, not collaborators,” Ashley Willis, Sr. Director of Developer Relations at GitHub, on How DevRel has Changed, Open Source, and Holding Space as a Leader cover image

“We were seen as amplifiers, not collaborators,” Ashley Willis, Sr. Director of Developer Relations at GitHub, on How DevRel has Changed, Open Source, and Holding Space as a Leader

Ashley Willis has seen Developer Relations evolve from being on the sidelines of the tech team to having a seat at the strategy table. In her ten years in the space, she’s done more than give great conference talks or build community—she’s helped shape what the DevRel role looks like for software providers. Now as the Senior Director of Developer Relations at GitHub, Ashley is focused on building spaces where developers feel heard, seen, and supported. > “A decade ago, we were seen as amplifiers, not collaborators,” she says. “Now we’re influencing product roadmaps and shaping developer experience end to end.” DevRel Has Changed For Ashley, the biggest shift hasn’t been the work itself—but how it’s understood. > “The work is still outward-facing, but it’s backed by real strategic weight,” she explains. “We’re showing up in research calls and incident reviews, not just keynotes.” That shift matters, but it’s not the finish line. Ashley is still pushing for change when it comes to burnout, representation, and sustainable metrics that go beyond conference ROI. > “We’re no longer fighting to be taken seriously. That’s a win. But there’s more work to do.” Talking Less as a Leader When we asked what the best advice Ashley ever received, she shared an early lesson she received from a mentor: “Your presence should create safety, not pressure.” > “It reframed how I saw my role,” she says. “Not as the one with answers, but the one who holds the space.” Ashley knows what it’s like to be in rooms where it’s hard to speak up. She leads with that memory in mind, and by listening more than talking, normalizing breaks, and creating environments where others can lead too. > “Leadership is emotional labor. It’s not about being in control. It’s about making it safe for others to lead, too.” Scaling More Than Just Tech Having worked inside high-growth companies, Ashley knows firsthand: scaling tech is one thing. Scaling trust is another. > “Tech will break. Roadmaps will shift. But if there’s trust between product and engineering, between company and community—you can adapt.” And she’s learned not to fall for premature optimization. Scale what you have. Don’t over-design for problems you don’t have yet. Free Open Source Isn’t Free There’s one myth Ashley is eager to debunk: that open source is “free.” > “Open source isn’t free labor. It’s labor that’s freely given,” she says. “And it includes more than just code. There’s documentation, moderation, mentoring, emotional care. None of it is effortless.” Open source runs on human energy. And when we treat contributors like an infinite resource, we risk burning them out, and breaking the ecosystem we all rely on. > “We talk a lot about open source as the foundation of innovation. But we rarely talk about sustaining the people who maintain that foundation.” Burnout is Not Admirable Early in her career, Ashley wore burnout like a badge of honor. She doesn’t anymore. > “Burnout doesn’t prove commitment,” she says. “It just dulls your spark.” Now, she treats rest as productive. And she’s learned that clarity is kindness—especially when giving feedback. > “I thought being liked was the same as being kind. It’s not. Kindness is honesty with empathy.” The Most Underrated GitHub Feature? Ashley’s pick: personal instructions in GitHub Copilot. Most users don’t realize they can shape how Copilot writes, like its tone, assumptions, and context awareness. Her own instructions are specific: empathetic, plainspoken, technical without being condescending. For Ashley, that helps reduce cognitive load and makes the tool feel more human. > “Most people skip over this setting. But it’s one of the best ways to make Copilot more useful—and more humane.” Connect with Ashley Willis She has been building better systems for over a decade. Whether it’s shaping Copilot UX, creating safer teams, or speaking truth about the labor behind open source, she’s doing the quiet work that drives sustainable change. Follow Ashley on BlueSky to learn more about her work, her maker projects, and the small things that keep her grounded in a fast-moving industry. Sticker Illustration by Jacob Ashley....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co