DEENESH

C#/.NET

A .NET API is more than CRUD endpoints

The first endpoints are usually the easy part. The real engineering begins when an API needs reliable data access, useful diagnostics, background work, tests, and a repeatable path to production.

When I started building JobTrack, the basic idea was straightforward: create an API for recording job applications and tracking their progress.

It needed the endpoints most people would expect. A user should be able to create an application, update its status, view the details, filter the list, and remove a record. There also needed to be a dashboard showing a summary of the pipeline.

ASP.NET Core makes that first stage pleasantly direct. Controllers, dependency injection, model binding, validation, and Entity Framework Core provide a strong foundation without requiring much ceremony.

But a working set of CRUD endpoints is only the beginning of an API. The more interesting decisions appear when I ask what will make it dependable outside my local machine.

HTTP behavior is part of the design

An endpoint should do more than return JSON. Its response needs to tell the client what actually happened.

For JobTrack, a successful creation returns 201 Created and points to the new resource. A deletion returns 204 No Content. Invalid input produces 400 Bad Request, while a request for a record that does not exist returns 404 Not Found. Standard Problem Details responses give clients a consistent error contract.

None of those choices is technically complicated, but consistency matters. A frontend should not have to inspect a successful-looking response body to discover that an operation really failed. Clear HTTP behavior makes integration easier and gives the API a contract that other developers can understand.

Swagger and OpenAPI help make that contract visible. I still treat the implementation as the source of truth, but having interactive documentation shortens the feedback loop when testing endpoints or connecting a client.

Entity Framework does not remove database thinking

Entity Framework Core reduces repetitive data-access code, but it does not make database design automatic.

I still need to decide which fields are required, how long text values can be, whether a number needs decimal precision, how dates should be stored, and which queries need to be efficient. I use migrations to keep schema changes versioned alongside the application instead of relying on manual database edits.

For read-only queries, AsNoTracking avoids change-tracking work that the request does not need. Asynchronous queries keep request threads available while SQL Server is doing I/O. On update paths, I also account for the possibility that a record disappears between the initial lookup and the save.

These are small choices individually. Together, they are the difference between simply using an ORM and understanding the persistence layer underneath it.

Some work should not happen during a request

JobTrack needs to identify applications that have remained in the Applied stage for more than seven days. I could run that check whenever someone loads the dashboard, but then a maintenance task becomes tied to user traffic and response time.

Instead, I separated it into an Azure Functions project. A timer-triggered function runs on a schedule, queries SQL Server for follow-up candidates, and records the result through structured logs. A protected HTTP endpoint can report the current count when another system needs it.

That boundary keeps the API focused on interactive requests while the function owns scheduled work. It also introduces a real deployment consideration: the API and the Functions application are separate artifacts and must be configured and released accordingly.

Serverless code is useful here, but it is not automatically simpler in every situation. A background service inside the main application might be enough for a process that shares the same lifecycle and scaling needs. I prefer a separate function when the schedule, failure behavior, or deployment boundary benefits from being independent.

Tests should exercise behavior, not framework details

For this project, I use xUnit with an isolated EF Core in-memory database. The tests cover behavior such as creating and persisting an application, filtering by status, and handling a missing record.

The goal is not to prove that ASP.NET Core or Entity Framework works. Their maintainers already do that. I want confidence that my controller behavior, data mapping, and application decisions work together as intended.

An in-memory provider also has limits. It is useful for fast, isolated tests, but it cannot reproduce every SQL Server behavior. Provider-specific queries, constraints, transaction behavior, and performance still need testing against a real database when those details matter.

That tradeoff is worth being explicit about. A fast test suite is valuable, but only if the team understands what it does and does not prove.

Deployment belongs in the project

I do not consider an API finished when it runs from my IDE.

The JobTrack pipeline restores and builds the .NET solution, runs the API and Angular tests, builds the web client, and publishes separate API, Functions, and web artifacts. The API is deployed to Azure App Service. Connection strings, JWT signing keys, and other environment-specific settings stay outside the repository, with local development using appropriate configuration and user secrets.

Health checks, structured logging, OpenTelemetry, and optional Application Insights support are part of the same production mindset. When something fails, I want enough information to distinguish an application bug from a configuration, database, or infrastructure problem.

Automation does not eliminate deployment risk, but it makes the process repeatable. A documented pipeline is much easier to review and improve than a set of steps that lives in one developer’s memory.

What this project reinforced for me

.NET provides excellent tools for building APIs, but the framework cannot make the engineering decisions on my behalf.

I still have to define a clear contract, model the data carefully, choose the right boundary for background work, test meaningful behavior, protect configuration, and decide how the application will be built, observed, and deployed.

CRUD endpoints demonstrate that an application can accept requests. The work around them demonstrates whether the application is ready to be owned.