Free Ebook cover Serverless on Azure: Building with Azure Functions and Event Triggers

Serverless on Azure: Building with Azure Functions and Event Triggers

New course

10 pages

Azure Functions Project Structure and Local Development Workflow

Capítulo 2

Estimated reading time: 9 minutes

+ Exercise

Local prerequisites for an efficient inner loop

A productive Azure Functions workflow relies on a tight “inner loop”: edit code, run locally, trigger the function, debug, and repeat. To do that reliably, you need a few tools installed locally so your machine can emulate the Azure Functions runtime and connect to local emulators or real Azure resources.

Install Azure Functions Core Tools

Azure Functions Core Tools provides the local runtime host (the func CLI) used to create projects, run them locally, and publish later. Install the latest v4 Core Tools to match current Azure Functions runtime behavior.

  • Verify installation: func --version
  • Useful commands you will use often: func init, func new, func start

Install the language SDK/runtime

Install the SDK for the language you will use (for example, .NET SDK, Node.js, Python, or Java). The local Functions host will invoke your function code through that language worker/runtime.

  • .NET: install the .NET SDK version aligned with your project’s target framework.
  • Node.js: install an LTS version; ensure node --version works.
  • Python: install a supported Python version and ensure python --version works; use virtual environments.
  • Java: install a supported JDK and build tool (Maven/Gradle) if applicable.

Install VS Code and extensions

VS Code is a common choice because it integrates project scaffolding, local run/debug, and Azure deployment.

  • Azure Functions extension (for creating/running/debugging Functions).
  • Azure Account extension (for sign-in and resource browsing).
  • Language-specific extensions: C# Dev Kit, Python, Java, or Node tooling.

Create a new Azure Functions project

You can scaffold a project either from the command line (repeatable and CI-friendly) or from VS Code (guided UI). The result is the same: a folder containing host configuration plus one or more function entry points.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

Option A: Create from the CLI

1) Create a folder and initialize a Functions project:

mkdir MyFunctionsApp && cd MyFunctionsApp func init

2) Choose a worker runtime when prompted (for example: dotnet, node, python).

3) Add a function (choose a trigger template):

func new

4) Select a trigger (for example HTTP trigger, Timer trigger, Queue trigger) and provide a function name.

Option B: Create from VS Code

1) Open the Command Palette and run “Azure Functions: Create New Project”.

2) Pick a folder, language, and a trigger template.

3) VS Code will scaffold the project and can automatically create a debug configuration.

Understand the key project files

Azure Functions projects have a small set of important files that control runtime behavior, local configuration, and function entry points. Knowing what they do helps you debug issues quickly and structure code cleanly.

host.json: host-level runtime configuration

host.json configures behavior for the Functions host and certain bindings. It applies across all functions in the app. Typical uses include logging configuration, extension/binding settings, and concurrency controls (depending on runtime and language).

Example (illustrative):

{ "version": "2.0", "logging": { "logLevel": { "default": "Information", "Host.Results": "Information", "Function": "Information" } } }

Keep host.json focused on runtime concerns. Avoid putting environment-specific secrets here.

local.settings.json: local-only app settings and connection strings

local.settings.json stores settings used when running locally, including Values (environment variables) and connection strings for triggers/bindings. This file is intended for local development only and should not be committed to source control when it contains secrets.

Common patterns:

  • Values contains settings like AzureWebJobsStorage, feature flags, and your own configuration keys.
  • Use separate settings per developer via local overrides, or provide a sanitized template file (for example local.settings.template.json) in the repo.

Example:

{ "IsEncrypted": false, "Values": { "FUNCTIONS_WORKER_RUNTIME": "dotnet", "AzureWebJobsStorage": "UseDevelopmentStorage=true", "MyApi__BaseUrl": "https://localhost:5005", "MyFeatureFlag": "true" } }

Note the double-underscore convention (MyApi__BaseUrl) which maps cleanly to hierarchical configuration in many frameworks.

Function entry points: where triggers connect to your code

Each function has an entry point that the runtime calls when the trigger fires. The exact shape depends on language and model, but the principle is the same: the entry point should be thin and delegate to testable business logic.

Typical responsibilities of an entry point:

  • Accept trigger input (HTTP request, queue message, timer tick, etc.).
  • Validate and parse input.
  • Call application/business services.
  • Return output (HTTP response, queue output, etc.).

Keep trigger-specific code (bindings, request/response types, serialization) at the edges.

Run functions locally

Running locally starts the Functions host, loads your functions, and listens for triggers. This is the foundation of your inner loop.

Start the local host

From the project folder:

func start

The host will print discovered functions and their endpoints (for HTTP triggers). If something fails to load, the console output usually points to missing settings, missing storage configuration, or language runtime issues.

Common local settings you must have

  • FUNCTIONS_WORKER_RUNTIME: identifies the language worker.
  • AzureWebJobsStorage: required by many triggers/bindings; for local development you can use an emulator connection string if available, or a real Azure Storage account connection string.

Debugging locally in VS Code

Debugging lets you set breakpoints inside your function code and inspect variables when triggers fire.

Set up a debug configuration

VS Code typically generates a launch.json configuration when you create the project. If not, use the Azure Functions extension to add it. The debug configuration starts the Functions host and attaches the debugger to the language runtime.

Debug workflow

  • Set breakpoints in the function entry point and in the business logic it calls.
  • Start debugging (Run and Debug panel).
  • Trigger the function (HTTP call, queue message, timer simulation).
  • Step through code, inspect locals, and evaluate expressions.

If breakpoints are not hit, verify you are running the debug configuration (not just func start in a terminal), and confirm symbols/source maps are generated for your language.

Simulate triggers during local development

To keep the inner loop fast, you should be able to fire triggers on demand without deploying.

HTTP triggers: call endpoints directly

When the host starts, it prints the local URL for each HTTP function. You can invoke it with a browser, curl, or an API client.

Example with curl:

curl -i http://localhost:7071/api/Hello?name=Azure

Example POST with JSON:

curl -i -X POST http://localhost:7071/api/ProcessOrder -H "Content-Type: application/json" -d '{"orderId":"123","amount":42.5}'

Timer triggers: test without waiting

Timer triggers run on a schedule. For local testing, you can temporarily adjust the schedule to run more frequently (for example every 10 seconds) and then revert it. Keep these changes local or controlled via configuration so you do not accidentally deploy an aggressive schedule.

Queue/Service Bus/Event triggers: use emulators or real resources

For triggers that depend on external infrastructure, you have two practical approaches:

  • Use a local emulator where available (commonly for Azure Storage). Configure AzureWebJobsStorage accordingly.
  • Use a dedicated development resource in Azure (recommended for Service Bus, Event Hubs, Event Grid). Store connection strings in local.settings.json and rotate them as needed.

To simulate a queue trigger, enqueue a test message using a script, CLI, or a small helper tool. The function should pick it up immediately when the host is running.

Structure code for testability

Functions are easiest to maintain when the trigger handler is a thin adapter and the business logic lives in plain, testable components. This lets you unit test without running the Functions host and without needing trigger-specific types.

Separate trigger handlers from business services

Recommended layering:

  • Function entry point (adapter): deals with trigger input/output, logging scope, and translating to domain types.
  • Application/service layer: orchestrates use cases (for example, “process order”, “send notification”).
  • Domain logic: pure logic and validation, ideally with minimal dependencies.
  • Infrastructure clients: HTTP clients, storage repositories, messaging publishers.

In practice, the function entry point should do little more than: parse input, call a service, map result to output.

Example pattern (pseudocode)

// Function entry point (trigger adapter) public async Task<HttpResponse> Run(HttpRequest req) { var dto = Parse(req); var result = await _orderService.ProcessAsync(dto); return ToHttpResponse(result); } // Business logic (testable) public class OrderService { public Task<ProcessResult> ProcessAsync(OrderDto dto) { // validate, compute, call repositories/clients } }

With this split, you can unit test OrderService with standard test frameworks, mocking repositories/clients, without any Functions runtime involvement.

Dependency injection for clean composition

Use dependency injection (where supported by your chosen language/model) to provide services and clients to your function entry points. This enables:

  • Mocking dependencies in unit tests.
  • Centralized configuration of HTTP clients, serializers, and retry policies.
  • Clear separation between runtime wiring and business logic.

Environment-based configuration for local runs

Local development needs different settings than cloud environments. The goal is to make configuration predictable and safe: no secrets in code, no accidental production connections, and easy switching between dev/test environments.

Use app settings (environment variables) as the source of truth

When running locally, local.settings.json populates environment variables for the Functions host. Your code should read configuration from the environment/configuration system rather than hard-coding values.

  • Store connection strings and API keys in local.settings.json (local only).
  • In Azure, store the same keys as Function App application settings.
  • Keep key names consistent across environments so code does not change.

Use configuration binding and validation

Prefer strongly-typed configuration objects (where available) and validate them at startup or first use. This catches missing settings early in the inner loop.

Example keys:

  • MyApi__BaseUrl
  • MyApi__ApiKey
  • FeatureFlags__UseNewFlow

Provide a safe template for developers

To keep onboarding smooth without leaking secrets:

  • Commit a local.settings.template.json with placeholder values.
  • Add local.settings.json to .gitignore.
  • Document required keys in a short README section or in code comments near configuration binding.

Switching environments during local runs

If you need to run locally against different backends (for example, dev vs. test), avoid editing code. Instead:

  • Maintain multiple local settings files (for example local.settings.dev.json, local.settings.test.json) and copy/symlink the one you need to local.settings.json before running.
  • Or use shell environment variables to override specific keys for a session.

This keeps the inner loop fast while reducing the risk of committing secrets or accidentally pointing local runs at production resources.

Now answer the exercise about the content:

When an Azure Functions HTTP trigger is hard to unit test and tightly coupled to runtime types, which approach best improves testability while keeping the local development inner loop fast?

You are right! Congratulations, now go to the next page

You missed! Try again.

Testability improves when trigger handlers stay thin and delegate to plain services that can be unit tested without the Functions host. Dependency injection helps compose and mock those services, while trigger-specific code remains at the edges.

Next chapter

Triggers: Designing Event-Driven Entry Points (HTTP, Timer, Queue, Event Grid)

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.