Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 309 additions & 0 deletions docs/guides/csharp/serverless-rest-api-example.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
---
description: Use the Nitric framework to easily build and deploy C# REST APIs for AWS, Azure, or GCP
title_seo: Building your first API with C# and Nitric
tags:
- API
- Key Value Store
languages:
- csharp
published_at: 2023-06-16
updated_at: 2025-01-06
---

# Building a REST API with Nitric

This guide will show you how to build a serverless REST API with the Nitric framework using C#. The example API enables getting, setting, and editing basic user profile information using a Nitric [key value store](/keyvalue) to store user data. Once the API is created, we'll test it locally, then optionally deploy it to a cloud of your choice.

The API will provide the following routes:

| **Method** | **Route** | **Description** |
| ---------- | -------------- | -------------------------------- |
| `GET` | /profiles | Get all profiles |
| `GET` | /profiles/[id] | Get a specific profile by its Id |
| `POST` | /profiles | Create a new profile |
| `DELETE` | /profiles/[id] | Delete a profile |

There is also an extended section of the guide that adds file operations using a Nitric [bucket](/storage) to store and retrieve profile pictures. The extension adds these routes to the API:

| **Method** | **Route** | **Description** |
| ---------- | ----------------------------- | --------------------------------- |
| `GET` | /profiles/[id]/image/upload | Get a profile image upload URL |
| `GET` | /profiles/[id]/image/download | Get a profile image download URL |
| `GET` | /profiles/[id]/image/view | View the image that is downloaded |

## Prerequisites

- [.NET SDK](https://dotnet.microsoft.com/en-us/download/dotnet)
- The [Nitric CLI](/get-started/installation)
- _(optional)_ Your choice of an [AWS](https://aws.amazon.com), [GCP](https://cloud.google.com), or [Azure](https://azure.microsoft.com) account

## Getting started

Let's start by creating a new project for our API:

```bash
nitric new MyProfileApi dotnet-starter
cd MyProfileApi
```

The scaffolded project should have the following structure:

```text
MyProfileApi/
├── services/Hello
│ ├── Hello.cs
│ ├── Hello.csproj
dotnet.dockerfile
dontnet.dockerfile.dockerignore
nitric.yaml
README.md
```

## Building the API

Let's start by adding the profile model.

### Define the Profile Model

Create a new file `Main/Profile.cs`:

```csharp title:Main/Profile.cs
namespace Common;
public class Profile
{
public string Name { get; set; }
public int Age { get; set; }
}
```

### Define the API

Applications built with Nitric can contain many APIs, let's start by adding an API and a key value store to this project to serve as the public endpoint.

```csharp title:Main/Main.cs
using Application = Nitric.Sdk.Nitric;
using Nitric.Sdk.Resource;
using Nitric.Sdk.Service;
using System.Collections.Generic;
using Common;
using System;

var profiles = Application.KV<Profile>("profiles").Allow(
KeyValueStorePermission.Set,
KeyValueStorePermission.Get,
KeyValueStorePermission.Delete
);

var api = Application.Api("main");
```

Here we're creating an API named public and key value store named `profiles`, then requesting get and set permissions which allows our function to access the key value store.

<Note>
Resources in Nitric like `api` and `key-value store` represent high-level
cloud resources. When your app is deployed Nitric automatically converts these
requests into appropriate resources for the specific
[provider](/providers). Nitric also takes care of adding the IAM roles,
policies, etc. that grant the requested access. For example the `key-value`
store resource uses DynamoDB in AWS or FireStore on Google Cloud.
</Note>

### Create profiles with POST

Next we will add features that allow our API consumers to work with profile data.

<Note>
You could separate some or all of these handlers into their own files if you
prefer. For simplicity we'll group them together in this guide.
</Note>

```csharp title:Main/Main.cs
api.Post("/profiles", ctx =>
{
var profile = ctx.Req.Json<Profile>();
var id = Guid.NewGuid().ToString();
profiles.Set(id, profile);
ctx.Res.Json(new { Message = $"Profile with id {id} created." });
return ctx;
});
```

### Retrieve a profile with GET

```csharp title:Main/Main.cs
api.Get("/profiles/:id", ctx =>
{
var id = ctx.Req.PathParams["id"];
try
{
var profile = profiles.Get(id);
ctx.Res.Json(profile);
}
catch (Nitric.Sdk.Common.NotFoundException)
{
ctx.Res.Status = 404;
ctx.Res.Json(new { Message = "Profile not found" });
}

return ctx;
});
```

### Remove a profile with DELETE

```csharp title:Main/Main.cs
api.Delete("/profiles/:id", ctx =>
{
var id = ctx.Req.PathParams["id"];
try
{
profiles.Delete(id);
ctx.Res.Json(new { Message = $"Profile with id {id} deleted." });
}
catch (Nitric.Sdk.Common.NotFoundException)
{
ctx.Res.Status = 404;
ctx.Res.Json(new { Message = "Profile not found" });
}
return ctx;
});
```

### List all profiles with GET

```csharp title:Main/Main.cs
api.Get("/profiles", ctx =>
{
var keys = profiles.Keys();
var profilesList = new List<string>();

while (keys.MoveNext().GetAwaiter().GetResult())
{
profilesList.Add(keys.Current);
}
ctx.Res.Json(profilesList);
return ctx;
});
```

## Ok, let's run this thing!

Now that you have an API defined with handlers for each of its methods, it's time to test it locally.

```bash
nitric start
```

## Test the API

Below are some example requests you can use to test the API. You'll need to update all values in brackets `[]` and change the URL to your deployed URL if you're testing on the cloud.

### Create Profile

```bash
curl --location --request POST 'http://localhost:4001/profiles' \
--header 'Content-Type: text/plain' \
--data-raw '{
"name": "Peter Parker",
"age": "21",
"homeTown" : "Queens"
}'
```

### Fetch Profile

```bash
curl --location --request GET 'http://localhost:4001/profiles/[id]'
```

### Fetch All Profiles

```bash
curl --location --request GET 'http://localhost:4001/profiles'
```

### Delete Profile

```bash
curl --location --request DELETE 'http://localhost:4001/profiles/[id]'
```

## Deploy to the cloud

At this point, you can deploy the application to any supported cloud provider. Start by setting up your credentials and any configuration for the cloud you prefer:

- [AWS](/providers/pulumi/aws)
- [Azure](/providers/pulumi/azure)
- [Google Cloud](/providers/pulumi/gcp)

Next, we'll need to create a `stack`. Stacks represent deployed instances of an application, including the target provider and other details such as the deployment region. You'll usually define separate stacks for each environment such as development, testing and production.

```bash
nitric stack new dev aws
```

Continue by checking your stack file `nitric.dev.yaml` and adding in your preferred region, let's use `us-east-1`.

```yaml title:nitric.dev.yaml
# The nitric provider to use
provider: nitric/aws@latest
# The target aws region to deploy to
# See available regions:
# https://docs.aws.amazon.com/general/latest/gr/lambda-service.html
region: us-east-1
```

<Note>
Cloud deployments incur costs and while most of these resource are available
with free tier pricing you should consider the costs of the deployment.
</Note>

We can deploy our application using the following command:

```bash
nitric up
```

When the deployment is complete, go to the relevant cloud console and you'll be able to see and interact with your API. If you'd like to make changes to the API you can apply those changes by re-running the `up` command. Nitric will automatically detect what's changed and just update the relevant cloud resources.

When you're done testing your application you can tear it down from the cloud, use the `down` command:

```bash
nitric down
```

## Optional - Add profile image upload/download support

### Access profile buckets with permissions

```csharp title:Main/Main.cs
var images = Application.Bucket("images").Allow(BucketPermission.Read, BucketPermission.Write);
```

### Get a URL to upload a profile image

```csharp title:Main/Main.cs
api.Get("/profiles/:id/image/upload", ctx =>
{
var id = ctx.Req.Params["id"];
var url = images.File($"images/{id}/photo.png").GetUploadUrl();
ctx.Res.Json(new { Url = url });
return ctx;
});
```

### Get a URL to download a profile image

```csharp title:Main/Main.cs
api.Get("/profiles/:id/image/download", ctx =>
{
var id = ctx.Req.Params["id"];
var url = images.File($"images/{id}/photo.png").GetDownloadUrl();
ctx.Res.Json(new { Url = url });
return ctx;
});
```

## Wrapping up

With this guide, you've built a simple C# REST API with Nitric, tested it locally, and deployed it to the cloud. You can now expand on this API with additional functionality or integrate it into a larger system!
17 changes: 9 additions & 8 deletions src/components/code/CodeSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ export async function CodeSwitcher({
code.map((codeblock) => highlight(codeblock)),
)

const missingLangs = languages.filter(
(lang) => !highlighted.some((h) => h.lang === lang),
)
// TODO: Put this back when csharp references are updated
// const missingLangs = languages.filter(
// (lang) => !highlighted.some((h) => h.lang === lang),
// )

if (missingLangs.length) {
throw Error(
`CodeSwitcher missing languages: ${missingLangs.join(', ')} at: ${highlighted[0].meta || 'unknown'}`,
)
}
// if (missingLangs.length) {
// throw Error(
// `CodeSwitcher missing languages: ${missingLangs.join(', ')} at: ${highlighted[0].meta || 'unknown'}`,
// )
// }

if (tabs) {
const children = highlighted.map((h) => (
Expand Down
43 changes: 43 additions & 0 deletions src/components/icons/DotNetColour.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const DotNetLogoColour = (props: React.ComponentPropsWithoutRef<'svg'>) => {
return (
<svg
width="32"
height="32"
viewBox="0 0 256 288"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
>
<g>
<path
d="M255.569,84.452376 C255.567,79.622376 254.534,75.354376 252.445,71.691376 C250.393,68.089376 247.32,65.070376 243.198,62.683376 C209.173,43.064376 175.115,23.505376 141.101,3.86637605 C131.931,-1.42762395 123.04,-1.23462395 113.938,4.13537605 C100.395,12.122376 32.59,50.969376 12.385,62.672376 C4.064,67.489376 0.015,74.861376 0.013,84.443376 C0,123.898376 0.013,163.352376 0,202.808376 C0,207.532376 0.991,211.717376 2.988,215.325376 C5.041,219.036376 8.157,222.138376 12.374,224.579376 C32.58,236.282376 100.394,275.126376 113.934,283.115376 C123.04,288.488376 131.931,288.680376 141.104,283.384376 C175.119,263.744376 209.179,244.186376 243.209,224.567376 C247.426,222.127376 250.542,219.023376 252.595,215.315376 C254.589,211.707376 255.582,207.522376 255.582,202.797376 C255.582,202.797376 255.582,123.908376 255.569,84.452376"
fill="#A179DC"
fill-rule="nonzero"
></path>
<path
d="M128.182,143.241376 L2.988,215.325376 C5.041,219.036376 8.157,222.138376 12.374,224.579376 C32.58,236.282376 100.394,275.126376 113.934,283.115376 C123.04,288.488376 131.931,288.680376 141.104,283.384376 C175.119,263.744376 209.179,244.186376 243.209,224.567376 C247.426,222.127376 250.542,219.023376 252.595,215.315376 L128.182,143.241376"
fill="#280068"
fill-rule="nonzero"
></path>
<path
d="M255.569,84.452376 C255.567,79.622376 254.534,75.354376 252.445,71.691376 L128.182,143.241376 L252.595,215.315376 C254.589,211.707376 255.58,207.522376 255.582,202.797376 C255.582,202.797376 255.582,123.908376 255.569,84.452376"
fill="#390091"
fill-rule="nonzero"
></path>
<path
d="M201.892326,116.294008 L201.892326,129.767692 L215.36601,129.767692 L215.36601,116.294008 L222.102852,116.294008 L222.102852,129.767692 L235.576537,129.767692 L235.576537,136.504534 L222.102852,136.504534 L222.102852,149.978218 L235.576537,149.978218 L235.576537,156.71506 L222.102852,156.71506 L222.102852,170.188744 L215.36601,170.188744 L215.36601,156.71506 L201.892326,156.71506 L201.892326,170.188744 L195.155484,170.188744 L195.155484,156.71506 L181.6818,156.71506 L181.6818,149.978218 L195.155484,149.978218 L195.155484,136.504534 L181.6818,136.504534 L181.6818,129.767692 L195.155484,129.767692 L195.155484,116.294008 L201.892326,116.294008 Z M215.36601,136.504534 L201.892326,136.504534 L201.892326,149.978218 L215.36601,149.978218 L215.36601,136.504534 Z"
fill="#FFFFFF"
></path>
<path
d="M128.456752,48.625876 C163.600523,48.625876 194.283885,67.7121741 210.718562,96.0819435 L210.558192,95.808876 L169.209615,119.617159 C161.062959,105.823554 146.128136,96.5150717 128.996383,96.3233722 L128.456752,96.3203544 C102.331178,96.3203544 81.1506705,117.499743 81.1506705,143.625316 C81.1506705,152.168931 83.4284453,160.17752 87.3896469,167.094792 C95.543745,181.330045 110.872554,190.931398 128.456752,190.931398 C146.149522,190.931398 161.565636,181.208041 169.67832,166.820563 L169.481192,167.165876 L210.767678,191.083913 C194.51328,219.21347 164.25027,238.240861 129.514977,238.620102 L128.456752,238.625876 C93.2021701,238.625876 62.4315028,219.422052 46.0382398,190.902296 C38.0352471,176.979327 33.4561922,160.837907 33.4561922,143.625316 C33.4561922,91.1592636 75.9884604,48.625876 128.456752,48.625876 Z"
fill="#FFFFFF"
fill-rule="nonzero"
></path>
</g>
</svg>
)
}

DotNetLogoColour.displayName = 'DotNetLogoColour'

export default DotNetLogoColour
2 changes: 2 additions & 0 deletions src/components/icons/LanguageIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import GoColorLogo from './GoLogoColour'
import DartLogoNoTextColour from './DartLogoNoTextColour'
import { cn } from '@/lib/utils'
import type { Language } from '@/lib/constants'
import DotNetLogoColour from './DotNetColour'

interface LanguageIconProps {
name: Language
Expand All @@ -18,6 +19,7 @@ const icons: Record<Language, React.FC<{ className: string }>> = {
python: PythonColorLogo,
go: GoColorLogo,
dart: DartLogoNoTextColour,
csharp: DotNetLogoColour,
}

export const LanguageIcon: React.FC<LanguageIconProps> = ({
Expand Down
Loading
Loading