Learn to build server-side application with the Command Query Responsibility Segregation (CQRS) model.
This post focuses on building server-side applications following a stream-oriented architecture or the Command Query Responsibility Segregation (CQRS) model, where we build servers that resemble a Redux store, in a sense.
When servers are built in this manner, the focus is on writing logic that responds to the client’s actions. Actions can either be commands or queries. Commands are strictly for modifying data, while queries are for data retrieval. The third piece is events, which allow broadcasting information to other parts of the app when actions occur.
To begin, we will look at the drawbacks of the layered/CRUD (Create, Read, Update and Delete) architecture commonly used, followed by a deep dive into the CQRS pattern, its benefits and its drawbacks. We will then go on to build a simple web server using the community-provided @nestjs/cqrs module.
To follow along with this guide, it is assumed you have a basic understanding of the NestJS backend framework and are familiar with HTTP, RESTful APIs and CRUD.
Assuming you have the Nest CLI installed, let’s now bootstrap an application. Open your terminal and run the following command:
nest new cqrs-demo
We called our working directory cqrs-demo. The command above creates a new NestJS project there. Feel free to choose a directory name of your choice.
Next, let’s install the @nestjs/cqrs package, which will be the only one we will be needing. Assuming you are in your working directory, enter the following command:
npm i @nestjs/cqrs
Irrespective of the architecture used, virtually all types of web servers have three key pieces, which are as follows:
In this section, we will explore the CRUD/layered architecture, which is the simplest and most common way to build server-side applications. Here we will look at some of the advantages and disadvantages of this approach.
In CRUD applications, controllers receive the user’s request and pass it to services, which hold all the business logic. They also directly connect to the persistence layer to store and retrieve the user’s data as the case may be. When a response is received, controllers forward the response back to the client.
Here is a diagram showing the basic flow of the CQRS model.

This model also has controllers, services and repositories like the CRUD model, but where things start to get different is in the order of how these structures are expected to be used. When the client’s request is received by the controller, it is mapped to an action, which is just a plain object that could either be a query or a command.
Queries are objects that hold some payload that will be used to read data from the application. Do not mistake them for query parameters. A query object may include query parameters, though. For example, a query object to get a user’s information.
Commands are objects that hold a payload required to mutate application state and perform side effects. For example, a command could hold information to create a user. Query and command payloads are passed to their respective handler functions to perform the desired task.
Command or query handlers may also wish to notify the system by emitting events. Similarly, event handlers/listeners receive event payloads to do some post-processing on the server. Event listeners may also dispatch other events, commands or queries as well.
There are several differences between commands, queries and events, and some of them are as follows:
Let us now take a look at the advantages and disadvantages of the CQRS model.
Now to the fun part. We will be building a simple voting app following the CQRS pattern. This will expose a few endpoints that allow the user to create a poll, view a created poll and vote for a poll. The goal of our mini app will not be about trivial implementation details of the voting app, but rather to see firsthand the building blocks of the CQRS pattern and some of the internals of the @nestjs/cqrs module, which we will be using.
We have understood the basic building blocks of the CQRS/stream-oriented architecture model. Now, let’s explore some of the internals of how it works before we proceed to build our app.
To begin, we start by defining queries, commands and event payloads, which are plain classes with one or more instance variables. These classes are not part of NestJS’s dependency injection system—they are relevant to the @nestjs/cqrs module to remember the names of queries, commands and events. Developers typically create instances of these classes to set up the payload for an event, query or command.
Next, we define classes that will act as handlers for them. Query and command handlers are classes that must have an execute() method, while event handlers are classes that must have a handle() method. These methods receive the payload of the expected action.
As stated earlier, events can have multiple handlers. On the flip side, queries and commands can have only one handler. If multiple handlers are registered for a command or query, only the latest one is used. Handlers need to be decorated to help the @nestjs/cqrs module identify them.
Next, we notify NestJS about handler classes by registering them in their modules since they are still regular providers.
Let us now explore some of the internals of the @nestjs/cqrs module.
A NestJS application is just a tree of modules. Each module may have one or more providers and may reference other modules and their providers. The @nestjs/cqrs is also a module of its own.
Let’s take a look at the diagram below.

The diagram above shows a simplified NestJS application tree with four modules: the root app module, the CQRS module, our polls module and a random module. Notice that we showed the providers contained in each module. We omitted the providers in the @nestjs/cqrs module for brevity. Let us now do a walk-through of how this module does its magic.
When the NestJS application starts, the @nestjs/cqrs module starts by traversing the NestJS application tree. Three separate functions, one for each category, traverse the tree separately to locate modules that have providers that are query handlers, command handlers or event handlers, respectively. Each function can distinguish handlers for its own category because of a special decorator we use to annotate handler classes when we create them in code.
Upon retrieval of providers/handlers for each category, control is then passed to the query bus, the command bus and the event bus. These three are the central points of all our interactions with the @nestjs/cqrs module.
The query bus and the command bus use a map data structure internally. The command bus maps commands to their handlers, and the query bus does likewise for queries. This is a one-to-one mapping, so there can only be one handler per query and one handler per command, as shown below:

So whenever we want to dispatch a query, we interact directly with the query bus and feed it the query payload. It checks its internal map, locates the handler and then invokes it. The same idea applies to the command bus.
This is why queries or commands are executed in the request context—that is, the result from their handlers can be sent back to the client.
If a command or query is dispatched and a handler is not found in the map data structure registry, an error is thrown.

The developer may handle them manually or propagate them to an exception filter registered in the system to send a response to the client.
The event bus is quite different, as shown below:

The core of the event bus uses observables and executes asynchronously. By default, it starts with one observable stream. Then, each event handler creates a new observable from the root that listens for a particular event and subscribes to it.
This model allows one event to have many listeners and all event handlers to be connected to one central event stream source.
For example, when an event X comes to the event bus, it is fed to the root observable stream, which is then propagated to child streams. Listeners for X then proceed to execute their logic to react to it, while listeners not interested in X simply ignore it.
If the logic in an event handler generates an error that is not handled, there is a separate observable to which the error is propagated. The developer can subscribe to this observable and handle the errors correctly.
We can directly interact with the event bus or one of its abstractions, such as the event publisher. More on this will be discussed later.
Let us now proceed to build our mini app using the @nestjs/cqrs module. We will be doing the following:
Our application is going to have one module called PollsModule. Using the Nest CLI, let’s create this module. Run the following command in your terminal:
nest g mo polls
Unless explicitly stated, all terminal commands we will be writing from here on assume we are in the polls folder.
Next, in the newly created polls folder, locate the polls.module.ts file and add the following to it:
import { Module } from "@nestjs/common";
import { PollsController } from "./controllers/polls.controller";
import { PollsService } from "./service/polls-service";
@Module({
controllers: [PollsController],
providers: [PollsService],
})
export class PollsModule {}
For now, our PollsModule has the PollsController and PollService. Let’s proceed to create these files:
mkdir controllers && touch controllers/polls.controller.ts
mkdir serviecs && touch services/pollsService.tsx
Update the polls.controller.ts file with the following:
import {
Body,
Controller,
Delete,
Get,
HttpStatus,
Param,
Patch,
Post,
} from "@nestjs/common";
@Controller("polls")
export class PollsController {
constructor() {}
@Post("create")
async createPoll() {}
@Post("/vote")
votePoll() {}
@Get(":id")
getPoll(@Param("id") id: string) {}
}
The controller contains three endpoints: to create a poll, vote for a poll and retrieve a poll by its ID.
Next, let us update the pollsService.tsx file with the following:
import { BadRequestException, NotFoundException } from "@nestjs/common";
import { Poll } from "../poll.interface";
export class PollsService {
private polls: Poll[] = [
{
"id": "1",
"name": "what is your favorite frontend javascript framework",
"createdAt": "2025-06-22T06:59:49.132Z",
"options": [
{
"option": "Angular",
"votes": 0
},
{
"option": "Vue",
"votes": 0
},
{
"option": "React",
"votes": 0
}
],
"ended": false
}
];
savePoll(poll: Poll): Poll {
this.polls.push(poll);
return poll;
}
findPollbyId(id: string) {
return this.polls.find(p => p.id === id);
}
votePoll(pollId: string, option: string): Poll | undefined {
const poll = this.findPollbyId(pollId)
if (!poll) throw new BadRequestException()
const opt = poll.options.find(o => o.option === option);
if (opt) opt.votes++;
return poll;
}
}
Our PollService holds functions that allow us to create a poll, find a poll by its ID and vote for a poll. We made use of an in-memory array of polls and not an actual database to keep things simple.
For context on what a Poll looks like, the poll.interface file holds a few type definitions:
export interface PollOption {
option: string;
votes: number;
}
export interface Poll {
id: string;
name: string;
createdAt: Date;
options: PollOption[];
ended: boolean;
}
Update the src/app.module.ts file to look like so:
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { CqrsModule } from "@nestjs/cqrs";
import { PollsModule } from "./polls/polls.module";
@Module({
imports: [CqrsModule.forRoot(), PollsModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
In the code above, we mounted two modules to our application tree: the PollsModule and the CqrsModule, using its forRoot() method. We will explain some of the internals of the forRoot() method in the following sections.
Queries allow us to read data. Our app will have one query that allows us to read a poll. When creating a query, we need to define the query payload and its handler function.
Assuming you are in the polls folder, run the following commands:
mkdir -p queries/get-poll
cd queries/get-poll
touch get-poll.query.ts get-poll.handler.ts
Now, update the get-poll.query.ts file to match the following:
import { Query } from "@nestjs/cqrs";
import { Poll } from "src/polls/poll.interface";
export class GetPollQuery extends Query<Poll> {
constructor(public readonly id: string) {
super();
}
}
Our query payload expects the identifier of the poll, and it expects its handler to return a poll. By extending the Query class and feeding the Poll as a type argument, we are able to specify the return type for the handler.
Next, let’s update the get-poll.handler.ts file with the following:
import { Injectable, NotFoundException } from "@nestjs/common";
import { IEventHandler, IQueryHandler, QueryHandler } from "@nestjs/cqrs";
import { GetPollQuery } from "./get-poll.query";
import { PollsService } from "src/polls/service/polls-service";
import { Poll } from "src/polls/poll.interface";
@QueryHandler(GetPollQuery)
export class GetPollHandlerService implements IQueryHandler<GetPollQuery> {
constructor(private readonly pollsRepository: PollsService) {}
async execute(event: GetPollQuery): Promise<Poll> {
const poll = this.pollsRepository.findPollbyId(event.id);
if (!poll) {
throw new NotFoundException(`Poll with id ${event.id} not found`);
}
return poll;
}
}
By passing the GetPollQuery class to the @QueryHandler decorator, it delegates the GetPollHandlerService class as the handler of GetPollQuery commands.
Every query handler must implement the IQueryHandler interface, which has only one method called execute. This method receives the query payload as a parameter. In our case, the function retrieves the poll data or throws an error if it does not get it.
We also need to register our GetPollHandlerService so that the CQRS module knows about it. Let us now include it in our PollsModule.
Update the polls.module.ts file with the following:
import { GetPollHandlerService } from "./queries/get-poll/get-poll.handler";
i;
@Module({
controllers: [PollsController],
providers: [PollsService, GetPollHandlerService],
})
export class PollsModule {}
Finally, in our controller, let us now map the client’s request to a GetPollQuery command:
import {
Body,
Controller,
Delete,
Get,
HttpStatus,
Param,
Patch,
Post,
} from "@nestjs/common";
import { CommandBus, QueryBus } from "@nestjs/cqrs";
import { CreatePollCommand } from "../commands/create-poll/create-poll.command";
import { GetPollQuery } from "../queries/get-poll/get-poll.query";
import { VotePollCommand } from "../commands/vote-poll/vote-poll.command";
import { CreatePollDto } from "../dto/create-poll.dto";
@Controller("polls")
export class PollsController {
constructor(private readonly queryBus: QueryBus) {}
@Get(":id")
getPoll(@Param("id") id: string) {
const poll = this.queryBus.execute(new GetPollQuery(id));
return poll;
}
}
The code above starts by injecting the query bus, and in the route handler, we dispatch the GetPollQuery query. Internally, the query bus retrieves the GetPollQueryHandler instance and feeds it the query payload, which retrieves the poll data and returns it.
To test the running application, open your terminal and run the following command:
npm run start:dev
Let us now proceed and test our newly created endpoint:

Just to reiterate, commands are state-changing operations. Our app will have two commands, which are:
We will defer the vote poll command to the next section when we discuss events. For now, let us focus on creating polls. We will need to do two things:
In our app, all poll commands will live in the polls/command folder.
Assuming you are in the polls folder, run the following command:
mkdir -p command/create-poll
cd command/create-poll/
touch create-poll.command.ts create-poll.handler.ts
The command above creates a file to hold the create poll command and its handler function.
Update the create-poll.command.ts file to look like so:
import { Command } from "@nestjs/cqrs";
import { CreatePollDto } from "../../dto/create-poll.dto";
export class CreatePollCommand extends Command<{ id: string }> {
constructor(public pollData: CreatePollDto) {
super();
}
}
The CreatePoll command is simply a class that has a public pollData property, which is a DTO that looks like so:
mkdir -p command/dto
touch dto/create-poll.ts
Update the dto/create-poll.ts file with the following:
export class CreatePollDto {
name: string;
options: string[];
}
The name is the name of the poll, and options are the allowed options that can be voted for in the poll.
Similar to queries, when defining commands, we can optionally specify the return type when the command gets executed. Notice that we had to extend the Command class to specify this. In our case, we expect that our dummy create poll command will return an object that includes the created poll ID.
Next, let us define the command handler. Proceed to update the create-poll.handler.ts file with the following:
import { Injectable } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreatePollCommand } from './create-poll.command';
import { PollsService } from 'src/polls/service/polls-service';
import { Poll } from 'src/polls/poll.interface';
@CommandHandler(CreatePollCommand)
export class CreatePollHandlerService implements ICommandHandler<CreatePollCommand> {
constructor(private readonly pollService: PollsService) { }
async execute(command: CreatePollCommand): Promise<{ id: string; }> {
const { name, options } = command.pollData;
const poll: Poll = {
id: (Math.random()).toString(36).replace(".", ""),
name,
createdAt: new Date(),
options: options.map(option => ({ option, votes: 0 })),
ended: false
};
this.pollService.savePoll(poll);
return { id: poll.id }
}
}
The @CommandHandler decorator receives a Command as its parameter. It allows us to label the CreatePollHandlerService class as a handler of the CreatePollCommand command.
Every command handler must implement the ICommandHandler interface, which has only one method called execute. This method receives the command payload as its argument. Ours just creates a dummy poll and stores it in our polls array using the injected PollService.
Notice that it had to return an object with an id property, which was what we specified as the return type when we created the command payload instance.
Next, let’s register the command handler to our PollsModule:
import { GetPollHandlerService } from "./queries/get-poll/get-poll.handler";
import { CreatePollHandlerService } from "./commands/create-poll/create-poll-handler";
@Module({
controllers: [PollsController],
providers: [PollsService, GetPollHandlerService, CreatePollHandlerService],
})
export class PollsModule {}
Finally, in our controller, let us now map the client’s request to a CreatePollCommand:
Update the poll-controller.ts file with the following:
@Controller("polls")
export class PollsController {
constructor(private readonly commandBus: CommandBus) {}
@Post("create")
async createPoll(@Body() createPollDto: CreatePollDto) {
const res = await this.commandBus.execute(
new CreatePollCommand(createPollDto)
);
return {
message: "Poll created",
data: {
pollId: res.id,
},
};
}
}
We injected the command bus in our constructor, and in the create poll route, we extracted the request body and mapped it to the CreatePollCommand and dispatched it to the system. The command bus again retrieves the correct command handler and invokes its execute method and passes the command payload.
Again, let’s test our application. We created a file called create-poll.json that holds some data representing a poll:
{
"name": "who is your favorite footballer",
"options": ["messi", "ronaldo", "Saurez"]
}
Again, let’s test the endpoint:

In this section, we will explore events and the event bus that allows us to dispatch them. Our app will have two events:
As we have been operating so far, we are not so interested in the logic of our event handlers but rather how to get things to work. The goal here will be to show how we can directly and indirectly communicate with the event bus to dispatch events.
Communicating directly with the event bus simply implies injecting the event bus and interacting with it directly by calling its methods, like we did with the query and command bus. We may also indirectly communicate with the event bus by means of interacting with other abstracted entities that sit on top of it.
Let’s now update the create-poll.handler.ts file with the following:
import { CommandHandler, EventBus, ICommandHandler } from "@nestjs/cqrs";
import { CreatePollCommand } from "./create-poll.command";
import { PollsService } from "src/polls/service/polls-service";
import { Poll } from "src/polls/poll.interface";
import { PollCreatedEvent } from "src/polls/events/poll-created/poll-created.event";
@CommandHandler(CreatePollCommand)
export class CreatePollHandlerService
implements ICommandHandler<CreatePollCommand>
{
constructor(
private readonly pollService: PollsService,
private readonly eventBus: EventBus
) {}
async execute(command: CreatePollCommand): Promise<{ id: string }> {
const { name, options } = command.pollData;
const poll: Poll = {
id: Math.random().toString(36).replace(".", ""),
name,
createdAt: new Date(),
options: options.map((option) => ({ option, votes: 0 })),
ended: false,
};
this.pollService.savePoll(poll);
this.eventBus.publish(new PollCreatedEvent(poll.id));
return { id: poll.id };
}
}
The snippet shows that command handlers may also emit events. To dispatch an event after we create a poll, we start by injecting the event bus, then we call its publish() method and pass our not yet created PollCreated event instance.
All our events will live in a folder called events. Run the following commands in your terminal:
mkdir -p events/poll-created
cd events/poll-created
touch poll-created.event.ts poll-created.handler.ts
Update the poll-created.event.ts file with the following:
export class PollCreatedEvent {
constructor(public readonly pollId: string) {}
}
Next, let us update the poll-created.handler.ts file to handle the event:
import { Injectable } from '@nestjs/common';
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { PollCreatedEvent } from './poll-created.event';
@EventsHandler(PollCreatedEvent)
export class PollCreatedEventHandlerService implements IEventHandler<PollCreatedEvent> {
async handle(event: Poll created event) {
console.log("yay! a new poll was created");
}
}
The @EventsHandler decorator marks the PollCreatedEventHandlerService as an event handler for the PollCreated event. All event handlers must implement the IEventHandler interface, which has only one method called handle().
Again, we need to register our event handler as a provider:
import { PollCreatedEventHandlerService } from "./events/poll-created/poll-created.handler";
@Module({
controllers: [PollsController],
providers: [
PollsService,
GetPollHandlerService,
CreatePollHandlerService,
PollCreatedEventHandlerService,
],
})
export class PollsModule {}
Now when we restart our NestJS application and try to create a poll, we see the dummy message printed by the event handler as shown below:

Here we will explore the AggregateRoot class and the EventPublisher class that ship with the @nestjs/cqrs module used to interact with the event bus. Here is an image showing a simple flow between the three:

When building applications following DDD (domain-driven design), we may have a model that contains some business logic which may need to dispatch events, and the model may not be able to inject dependencies.
To understand this indirect communication, let us now create our vote command. In the polls folder, run the following commands:
mkdir -p commands/vote-poll
touch ./votepoll/vote-poll.command.ts ./votepoll/vote-poll.handler.ts
Update the vote-poll.command.ts file with the following:
export class VotePollCommand {
constructor(public readonly pollId: string, public readonly option: string) {}
}
Next, update votepoll/vote-poll.handler.ts file with the following:
import { Injectable } from "@nestjs/common";
import {
AggregateRoot,
CommandHandler,
EventPublisher,
ICommandHandler,
} from "@nestjs/cqrs";
import { VotePollCommand } from "./vote-poll.command";
import { PollsService } from "src/polls/service/polls-service";
import { VotedEvent } from "src/polls/events/voted/voted.event";
@CommandHandler(VotePollCommand)
export class VotePollHandlerService
extends AggregateRoot
implements ICommandHandler<VotePollCommand>
{
constructor(
private readonly pollRepository: PollsService,
private publisher: EventPublisher
) {
super();
}
async execute(command: VotePollCommand): Promise<void> {
const { pollId, option } = command;
this.pollRepository.votePoll(pollId, option);
this.publisher.mergeObjectContext(this);
console.log("voted");
this.apply(new VotedEvent(pollId));
this.commit();
}
}
By making our model class extend the AggregateRoot class, it will now additionally contain two abstract methods: publish and publishAll.
We also injected the EventPublisher, which has two main methods: mergeClassContext() and mergeObjectContext(). These functions expect a class or an object respectively as parameters, and internally connect them to the event bus. It does this by defining concrete definitions for the publishAll() and publish() methods, which in turn invoke those of the event bus.
By making VotePollHandlerService extend the AggregateRoot class and having connected it to the event bus via the this.publisher.mergeObjectContext(this) call, it can now emit events by calling the apply() method, which accepts a variable number of events which are then batched. We call commit(), which will then in turn publish them to the event bus stream. We may also set up auto-commits so that the apply() call automatically publishes them.
In the code above, the VotePollHandlerService also emits an event called Voted. We can handle the event by defining an event listener using the pattern described earlier. However, I will introduce another way to write one or more listeners using a saga.
All our sagas will be in a folder called sagas:
mkdir sagas
touch ./sagas/poll-sagas.ts
Update the ./sagas/poll-sagas.ts file with the following:
import { Injectable } from "@nestjs/common";
import { ofType, Saga } from "@nestjs/cqrs";
import { map, Observable, tap } from "rxjs";
import { VotedEvent } from "../events/voted/voted.event";
import { PublishPollResultCommand } from "../commands/publish-result/publish-result.command";
@Injectable()
export class PollSagas {
@Saga()
userVoted(events$: Observable<any>): Observable<PublishPollResultCommand> {
return events$.pipe(
ofType(VotedEvent),
tap((e) => console.log(`Poll ${e.pollId} ended event received`)),
map((event) => new PublishPollResultCommand(event.pollId))
);
}
}
Sagas are providers that allow us to listen to one or more events. In the code above, we have one saga called userVoted. Sagas must use the @Saga decorator. The goal of sagas is to listen for events and then dispatch commands. The saga function will receive the event stream as its input. Here we listen to the VotedEvent and then map that event to another command called PublishPollResultCommand to publish the result of the vote to the user.
Our publish event looks like this:
// commands/publish-result/publish-poll-result.command.ts
import { Command } from "@nestjs/cqrs";
export class PublishPollResultCommand extends Command<boolean> {
constructor(public readonly pollId: string) {
super();
}
}
Its handler logic which just prints a message looks like this:
commands / publish - result / publish - poll - result.handler.ts;
import { CommandHandler, ICommandHandler } from "@nestjs/cqrs";
import { PublishPollResultCommand } from "./publish-result.command";
@CommandHandler(PublishPollResultCommand)
export class PublishPollResultCommandHandler
implements ICommandHandler<PublishPollResultCommand>
{
async execute(command: PublishPollResultCommand): Promise<boolean> {
console.log("sending message to voter about poll results...");
return true;
}
}
To conclude, let’s add all our providers to our poll.module.ts file:
import { Module } from "@nestjs/common";
import { PollsController } from "./controllers/polls.controller";
import { CreatePollHandlerService } from "./commands/create-poll/create-poll-handler";
import { PollsService } from "./service/polls-service";
import { GetPollHandlerService } from "./queries/get-poll/get-poll.handler";
import { VotePollHandlerService } from "./commands/vote-poll/vote-poll.handler";
import { PollSagas } from "./sagas/poll.sagas";
import { PublishPollResultCommandHandler } from "./commands/publish-result/publish-poll-result.handler";
import { PollCreatedEventHandlerService } from "./events/poll-created/poll-created.handler";
@Module({
controllers: [PollsController],
providers: [
PollsService,
GetPollHandlerService,
CreatePollHandlerService,
VotePollHandlerService,
PollSagas,
PublishPollResultCommandHandler,
PollCreatedEventHandlerService,
],
})
export class PollsModule {}
To trigger our vote endpoint we created some dummy payload in the vote.json file.
{
"poll_id": "1",
"option": "React"
}
Run the following command in your terminal:
curl --json @vote.json http://localhost:3000/polls/vote
You should see the message from the vote command handler, the saga and the poll ended handler.

CQRS provides a better option to build larger server-side applications compared to CRUD and pairs really well with domain-driven design. It gives us a different way of thinking about server-side applications.
Even though it involves writing more code and requires extra understanding, it gives the developer foresight on options available when building server-side applications and helps them make better decisions when faced with architectural problems.
Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.