Wednesday, February 5, 2025

Develop simple REST API with PHP and MySQL

 Overview

In this article we will develop is simple REST API with PHP. The database backend is MySQL running in a Docker container and the sample data represents students.

Source code: https://github.com/medhatelmasry/school-api

Getting Started

Start MySQL in a Docker container with:

docker run -d -p 3333:3306 --name maria -e MYSQL_ROOT_PASSWORD=secret mariadb:10.7.3

In a working directory named school-api, create the following sub-folders:

mkdir src
cd src
mkdir Controller
mkdir System
mkdir TableGateways
cd ..
mkdir public

Add a composer.json file in the top directory with just one dependency: the DotEnv library which will allow us to keep our authentication details in a .env file outside our code repository:


Contents of the composer.json file:

{
    "require": {
        "vlucas/phpdotenv": "^2.4"
    },
    "autoload": {
        "psr-4": {
            "Src\\": "src/"
        }
    }
}

We also configured a PSR-4 autoloader which will automatically look for PHP classes in the /src directory.

We can install our dependencies with:

composer install

We now have a /vendor directory, and the DotEnv dependency is installed (we can also use our autoloader to load our classes from /src with no include() calls).

Let’s create a .gitignore file for our project with two lines in it, so the /vendor directory and our local .env file are ignored if our code is pushed to source control. Add this to .gitignore:

vendor/
.env

Next, add a .env file where we’ll put our database connection details:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3333
DB_DATABASE=apidb
DB_USERNAME=root
DB_PASSWORD=secret

Add a bootstrap.php file to load environment variables.

<?php
error_reporting(E_ALL & ~E_DEPRECATED);

require 'vendor/autoload.php';

use Dotenv\Dotenv;

use Src\System\DatabaseConnector;

$dotenv = new DotEnv(__DIR__);
$dotenv->load();

$dbConnection = (new DatabaseConnector())->getConnection();

Configure DB for PHP REST API

We can now create a class to hold our database connection and add the initialization of the connection to our bootstrap.php file. In the src/System/ folder, add a file named DatabaseConnector.php with this code:

<?php
namespace Src\System;
class DatabaseConnector {
    private $dbConnection = null;
    public function __construct() {
        $host = getenv('DB_HOST');
        $port = getenv('DB_PORT');
        $db   = getenv('DB_DATABASE');
        $user = getenv('DB_USERNAME');
        $pass = getenv('DB_PASSWORD');
        try {
            $pdo = new \PDO("mysql:host=$host;port=$port;charset=utf8mb4", $user, $pass);
            $pdo->exec("CREATE DATABASE IF NOT EXISTS `$db`");
        } catch (\PDOException $e) {
            die("DB ERROR: " . $e->getMessage());
        }
        try {
            $this->dbConnection = new \PDO(
                "mysql:host=$host;port=$port;charset=utf8mb4;dbname=$db",
                $user,
                $pass
            );
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }
    }
         public function getConnection() {
        return $this->dbConnection;
    }
}

Let’s create a dbseed.php file which creates a students table and inserts some records for testing. Code for dbseed.php is shown below:

<?php
require 'bootstrap.php';
$statement = "
    CREATE TABLE IF NOT EXISTS students (
        id INT NOT NULL AUTO_INCREMENT,
        firstname VARCHAR(40) NOT NULL,
        lastname VARCHAR(40) NOT NULL,
        school VARCHAR(40) NOT NULL,
        PRIMARY KEY (id)
    );
    INSERT INTO students
        (firstname, lastname, school)
    VALUES
        ('Mark', 'Fisher','Computing'),
        ('Lisa', 'Fisher','Computing'),
        ('Judy', 'Fisher','Computing'),
        ('Jane', 'Smith','Nursing'),
        ('Mary', 'Smith','Nursing'),
        ('Andy', 'Smith','Nursing'),
        ('Bill', 'Smith','Business'),
        ('Fred', 'Plumber','Business'),
        ('Anna', 'Plumber','Business');
";
try {
    $createTable = $dbConnection->exec($statement);
    echo "Success!\n";
} catch (\PDOException $e) {
    exit($e->getMessage());
}

Run the following command to seed the database with sample data:

php dbseed.php

Add a Gateway Class for students table

In the src/TableGateways/StudentsGateway.php class file, we will implement methods to return all students, return a specific student and add/update/delete a student:

<?php
namespace Src\TableGateways;
class StudentsGateway {
    private $db = null;
    public function __construct($db) {
        $this->db = $db;
    }
    public function findAll() {
        $statement = "
            SELECT 
                id, firstname, lastname, school
            FROM
                students;
        ";
        try {
            $statement = $this->db->query($statement);
            $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
            return $result;
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }
    }
    public function find($id) {
        $statement = "
            SELECT 
                id, firstname, lastname, school
            FROM
                students
            WHERE id = ?;
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array($id));
            $result = $statement->fetchAll(\PDO::FETCH_ASSOC);
            return $result;
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
    public function insert(Array $input) {
        $statement = "
            INSERT INTO students 
                (firstname, lastname, school)
            VALUES
                (:firstname, :lastname, :school);
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array(
                'firstname' => $input['firstname'],
                'lastname'  => $input['lastname'],
                'school' => $input['school'] ?? null,
            ));
            return $statement->rowCount();
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
    public function update($id, Array $input) {
        $statement = "
            UPDATE students
            SET 
                firstname = :firstname,
                lastname  = :lastname,
                school = :school
            WHERE id = :id;
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array(
                'id' => (int) $id,
                'firstname' => $input['firstname'],
                'lastname'  => $input['lastname'],
                'school' => $input['school'] ?? null,
            ));
            return $statement->rowCount();
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
    public function delete($id) {
        $statement = "
            DELETE FROM students
            WHERE id = :id;
        ";
        try {
            $statement = $this->db->prepare($statement);
            $statement->execute(array('id' => $id));
            return $statement->rowCount();
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }    
    }
}

Implement the PHP REST API

Our REST API will have the following endpoints:

return all recordsGET /students
return a specific recordGET /students/{id}
create a new recordPOST /students
update an existing recordPUT /students/{id}
delete an existing recordDELETE /students/{id}

Create a src/Controller/StudentsController.php to handle the API endpoints with this code:

<?php
namespace Src\Controller;
use Src\TableGateways\StudentsGateway;
class StudentsController {
    private $db;
    private $requestMethod;
    private $id;
    private $studentsGateway;
    public function __construct($db, $requestMethod, $id) {
        $this->db = $db;
        $this->requestMethod = $requestMethod;
        $this->id = $id;
        $this->studentsGateway = new StudentsGateway($db);
    }
    public function processRequest() {
        switch ($this->requestMethod) {
            case 'GET':
                if ($this->id) {
                    $response = $this->getById($this->id);
                } else {
                    $response = $this->getAll();
                };
                break;
            case 'POST':
                $response = $this->createRequest();
                break;
            case 'PUT':
                $response = $this->updateFromRequest($this->id);
                break;
            case 'DELETE':
                $response = $this->deleteById($this->id);
                break;
            default:
                $response = $this->notFoundResponse();
                break;
        }
        header($response['status_code_header']);
        if ($response['body']) {
            echo $response['body'];
        }
    }
    private function getAll() {
        $result = $this->studentsGateway->findAll();
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = json_encode($result);
        return $response;
    }
    private function getById($id) {
        $result = $this->studentsGateway->find($id);
        if (! $result) {
            return $this->notFoundResponse();
        }
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = json_encode($result);
        return $response;
    }
    private function createRequest() {
        $input = (array) json_decode(file_get_contents('php://input'), TRUE);
        if (! $this->validate($input)) {
            return $this->unprocessableEntityResponse();
        }
        $this->studentsGateway->insert($input);
        $response['status_code_header'] = 'HTTP/1.1 201 Created';
        $response['body'] = null;
        return $response;
    }
    private function updateFromRequest($id) {
        $result = $this->studentsGateway->find($id);
        if (! $result) {
            return $this->notFoundResponse();
        }
        $input = (array) json_decode(file_get_contents('php://input'), TRUE);
        if (! $this->validate($input)) {
            return $this->unprocessableEntityResponse();
        }
        $this->studentsGateway->update($id, $input);
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = null;
        return $response;
    }
    private function deleteById($id) {
        $result = $this->studentsGateway->find($id);
        if (! $result) {
            return $this->notFoundResponse();
        }
        $this->studentsGateway->delete($id);
        $response['status_code_header'] = 'HTTP/1.1 200 OK';
        $response['body'] = null;
        return $response;
    }
    private function validate($input) {
        if (! isset($input['firstname'])) {
            return false;
        }
        if (! isset($input['lastname'])) {
            return false;
        }
        return true;
    }
    private function unprocessableEntityResponse() {
        $response['status_code_header'] = 'HTTP/1.1 422 Unprocessable Entity';
        $response['body'] = json_encode([
            'error' => 'Invalid input'
        ]);
        return $response;
    }
    private function notFoundResponse() {
        $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
        $response['body'] = null;
        return $response;
    }
}

Finally, create a /public/index.php file to serve as the front controller to process requests:

<?php
require "../bootstrap.php";

use Src\Controller\StudentsController;

header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE");
header("Access-Control-Max-Age: 3600");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");  
 
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = explode( '/', $uri ); 
 
// all of our endpoints start with /students
// everything else results in a 404 Not Found
if ($uri[1] !== 'students') {
    header("HTTP/1.1 404 Not Found");
    exit();
} 
 
// the id is, of course, optional and must be a number:
$id = null;
if (isset($uri[2])) {
    $id = (int) $uri[2];
} 
 
$requestMethod = $_SERVER["REQUEST_METHOD"]; 
 
// pass the request method and user ID to the StudentsController and process the HTTP request:
$controller = new StudentsController($dbConnection, $requestMethod, $id);
$controller->processRequest();

Start the web server with the following command, where switch -t starts the server in the public folder:

php -S 127.0.0.1:8888 -t public

Test your API with postman with these endpoints:

return all recordsGET http://localhost:8888/students
return a specific recordGET http://localhost:8888/students/{id}
create a new recordPOST http://localhost:8888/students
update an existing recordPUT / http://localhost:8888/students/{id}
delete an existing recordDELETE http://localhost:8888/students/{id}


Sunday, February 2, 2025

Using PHP with AI models hosted on GitHub

Overview

In this article I will show you how you can experiment with AI models hosted on GitHub in a simple PHP web app. GitHub AI Models are intended for learning, experimentation and proof-of-concept activities. The feature is subject to various limits (including requests per minute, requests per day, tokens per request, and concurrent requests) and is not designed for production use cases.

Prerequisites

To proceed, you will need the following:

  • PHP - You need to have PHP version 8.3 (or higher) installed on your computer. You can download the latest version from https://www.php.net/downloads.php.
  • Composer – If you do not have Composer yet, download and install it for your operating system from https://getcomposer.org/download/.

Getting Started

There are many AI models from a variety of vendors that you can choose from. The starting point is to visit https://github.com/marketplace/models. At the time of writing, these are a subset of the models available:


For this article, I will use the "DeepSeek-R1" beside the red arrow above. If you click on that model, you will be taken to the model's landing page:

Click on the green "Get API key" button.


The first thing we need to do is get a 'personal access token' by clicking on the “Get developer key” button.

Choose 'Generate new token', which happens to be in beta at the time of writing.


Give your token a name, set the expiration, and optionally describe the purpose of the token. Thereafter, click on the green 'Generate token' button at the bottom of the page.


Copy the newly generated token and place it is a safe place because you cannot view this token again once you leave the above page.

Let's do some PHP coding

In a working directory, create a sub-directory named PHP-GitHub-AI inside a terminal window with the following command:

mkdir PHP-GitHub-AI

Change into the newly created directory named PHP-GitHub-AI with:

cd PHP-GitHub-AI

In the PHP-GitHub-AI folder, create a file named index.php and add to it the following code:

<?php

// Set your Azure API key and endpoint
$apiKey = 'PUT-YOUR-PERSONAL-ACCESS-TOKEN-FROM-GITHUB-HERE';
$endpoint = 'https://models.inference.ai.azure.com';

// Define the API endpoint
$url = $endpoint . '/chat/completions';

// Set up the data for the API request
$data = [
    'messages' => [
        [
            'role' => 'system',
            'content' => 'you are an expert in astronomy'
        ],
        [
            'role' => 'user',
            'content' => 'which is the furthest planet to earth?'
        ]
    ],
    'model' => 'DeepSeek-R1',    // gpt-4o   DeepSeek-R1
    'temperature' => 1,
    'max_tokens' => 4096,
    'top_p' => 1
];
// Initialize cURL
$ch = curl_init($url);

// Set cURL options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: ' . $apiKey,
]);

curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

// Execute the API request
$response = curl_exec($ch);

// Check for errors
if ($response === false) {
    echo 'Error: ' . curl_error($ch);
} else {

    // Decode the response
    $result = json_decode($response, true);

    // Print the entire response for debugging
    /*
    echo '<pre>';
    print_r($result);
    echo '</pre>';
    */
    // Check if the 'choices' key exists in the response
    if (isset($result['choices'][0]['message']['content'])) {
        echo '<h3>Generated Text by ' . $result['model'] .':</h3>';
        // Print the generated text
        echo "<p>" . $result['choices'][0]['message']['content'] . "</p>";
    } else {
        if (isset($result['error'])) {
            echo 'Error: ' . $result['error']['message'];
        } else {
            echo 'Error: Unable to retrieve generated text.';
        }
    }
}

// Close cURL

curl_close($ch);
?>

In the above code:

  • Set the value of $apiKey to be the personal access token from GitHub
  • The system prompt is: 'you are an expert in astronomy'.
  • The user prompt is: 'which is the furthest planet to earth?'
  • We will be using the ‘DeepSeek-R1’ model

You can start the PHP web server in the PHP-GitHub-AI folder with this terminal window command:

php -S localhost:8888

Point your browser to http://localhost:8888. The output would look like this:


The output is in markdown format. We will need a library that converts from markdown to HTML. To that end, stop the web server and install the erusev/parsedown package into your application with this terminal window command:

composer require erusev/parsedown

Back in the code, make the following changes:
  • Add this code to the first line of your PHP code:
// composer require erusev/parsedown
require_once 'vendor/autoload.php';

  • Replace the following statement:

echo "<p>" . $result['choices'][0]['message']['content'] . "</p>";

WITH

error_reporting(E_ALL ^ E_DEPRECATED);
$Parsedown = new Parsedown();
$text =  $Parsedown->text($result['choices'][0]['message']['content']);
echo "<p>$text</p>";

Restart the web server with “php -S localhost:8888”. The page now shows a much better looking output:

You can change the AI model from DeepSeek-R1 to any other model (like gpt-4o) and will get similar results.

Tuesday, January 28, 2025

Built-in token authentication with ASP.NET Core 9.0 Minimal WebAPI

In this tutorial I will showcase the built-in WebApi token authentication. To keep it simple, we shall persist our data in the lightweight SQLite database.

Prerequisites

It is assumed that you have installed the following on your computer:
  • .NET 9.0
  • Visual Studio Code editor

Getting Started

In a suitable working directory, create a new WebApi application using the following terminal window command:

dotnet new webapi --name WebApiTokenAuth

Change directory with:

cd WebApiTokenAuth

We will need to add some packages. Also in the same terminal window, run the following commands:

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore

dotnet add package Microsoft.EntityFrameworkCore.Sqlite

dotnet add package Microsoft.EntityFrameworkCore.Tools

dotnet add package Microsoft.EntityFrameworkCore.Design

dotnet add package Swashbuckle.AspNetCore

The above packages allow us to use SQLite and Entity Framework. The last package (Swashbuckle.AspNetCore) will be used to provide a default Swagger UI.

Make sure you have the dotnet-ef tool. If you do not, you can globally install it with:

dotnet tool install --global dotnet-ef

If you already have the dotnet-ef, update your version to the latest with:

dotnet tool update --global dotnet-ef

Let us first add Swagger UI to our WebAPI application so that we can easily test our API endpoints. Open your source code with Visual Studio Code by executing the following statement from a terminal window inside the root folder of the application:

code .

You can now add the following statement right under app.MapOpenApi():

app.UseSwaggerUI(options =>  {
    options.SwaggerEndpoint("/openapi/v1.json", "My WebAPI");
});

Edit file Properties/launchSettings.json. In both http and https blocks, change the value of launchBrowser to true.

Also, in both http & https blocks, add the following setting:

"launchUrl": "swagger"

Let us run our web app and see what it does. Run the web app with:

dotnet watch

This gets loaded into your default browser:



Click on GET >> Try it out >> Execute. This will produce the following output:


Database context class

Since we will be using Entity Framework to talk to the SQLite database, we will need a database context class. Add a folder named Data, then add to it a class named ApplicationDbContext that derives from IdentityDbContext<IdentityUser> with the following code:

public class ApplicationDbContext: IdentityDbContext<IdentityUser> {
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {}
}

Add the following connection string to appsettings.json:

"ConnectionStrings": {
  "DefaultConnection": "Data Source=webapi-auth.db;"
}

Next, we must register ApplicationDbContext with our app by adding the following code into Program.cs right before 'var app = builder.Build();':

// Configure identity database access via EF Core
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));

Other required services

Add the following code right after the above code:

// Authorization
builder.Services.AddAuthorization();

// Activate identity APIs. By default, both cookies and proprietary tokens
// are activated. Cookies will be issued based on the 'useCookies' querystring
// parameter in the login endpoint
builder.Services.AddIdentityApiEndpoints<IdentityUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

We need to add authorization and authentication middleware with the following code right after 'app.UseHttpsRedirection();':

app.UseAuthentication(); 
app.UseAuthorization(); 

Let us secure the /weatherforecast endpoint by forcing authentication. Chain the following to the endpoint by adding this code right under '.WithOpenApi()':

.RequireAuthorization();

The full app.MapGet() code will look like this:

app.MapGet("/weatherforecast", () =>
{
    var forecast =  Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi()
.RequireAuthorization();

Adding Identity API endpoints

Add the identity endpoints to your app by calling MapIdentityApi<IdentityUser>(). Add the following code to Program.cs right before 'app.Run();':

app.MapIdentityApi<IdentityUser>();

Migrations

Since our app uses EF Core, we will create a migration in the Data folder and update the database. Run the following terminal commands:

dotnet ef migrations add M1 -o Data/Migrations

dotnet ef database update

You will notice that a SQLite database file is created named webapi-auth.db.

Try it out

Let us test our application to see whether or not we have indeed succeeded in securing our API endpoint. Start your app with:

dotnet watch

You will see a variety of identity related endpoints when the following page gets loaded into your default browser:


Try to hit the /weatherforecast endpoint and see the data. You will encounter a 401 (unauthorized) error:

Let us register a user. Click on the /register endpoint then click on the "Try it out" button. Update the JSON object so it looks like this:

{
  "email": "a@a.a",
  "password": "P@$$w0rd"
}

Click on the Execute button. You will get a Code 200 response representing success:


Next, let us login with the credentials we created. Click on the /login endpoint, then click on the "Try it out" button.


Choose true for useCookies and update the JSON object so it only has the credentials we had previously created. Thereafter, click on the Execute button. You should get a code 200 response:

Now let's try getting the data using the GET /weatherforecast endpoint. It should be a relief that we can now see the weather forecast information.

Conclusion

With ASP.NET Core 9.0 we need to follow these steps:
  • restore Swagger UI
  • configure our WebAPI application with the built in ,NET token authentication capability

Monday, January 20, 2025

Phi-3 Small Language Model (SLM) in a PHP app with Ollama and LLPhant framework

Overview

In this tutorial, we will see how easy it is to use the Phi-3 small language model in a PHP application. The best part is that it is free and runs entirely on your local device. Ollama is used to serve the Phi-3 small language model and LLPhant is the PHP framework for communicating with the AI model. 

Prerequisites

To proceed, you will need the following:

What is small language model (SLM)?

A small language model (SLM) is a machine learning model typically based on a large language model (LLM) but of greatly reduced size. An SLM retains much of the functionality of the LLM from which it is built but with far less complexity and computing resource demand.

What is Ollama?

Ollama is an application you can download onto your computer or server to run open-source generative AI small-language-models (SLMs) such as Meta's Llama 3 and Microsoft's Phi-3. You can see the many models available at https://www.ollama.com/library.

What is LLPhant

LLPhant is an open-source PHP Generative AI Framework at https://github.com/LLPhant/LLPhant

Getting Started

Download the Ollama installer from https://www.ollama.com/download.

Once you have installed Ollama, run these commands from a terminal window:

ollama pull phi3:latest
ollama list
ollama show phi3:latest

In a suitable working directory, create a folder named PhpAI with the following terminal window command:

mkdir PhpAI

Change into the newly created folder with:

cd PhpAI

Using Composer, install the theodo-group/llphant package by running this command:

composer require theodo-group/llphant

Let's get coding

Create a file named index.php with the following content:

<?php

require_once 'vendor/autoload.php';

use LLPhant\OllamaConfig;
use LLPhant\Chat\OllamaChat;  
 
$config = new OllamaConfig();
$config->model = 'phi3'; 
 
$chat = new OllamaChat($config); 
 
$chat->setSystemMessage('You are a helpful assistant who knows about world geography.'); 
 
$response = $chat->generateText('what is the capital of france?');  
 
echo $response;
?>

Running the app

To run the app, start the PHP web server to listen on port number 8888 with the following command in the PhpAI folder.

php -S localhost:8888

You can view the output by pointing your browser to the following URL:

http://localhost:8888/

This is what I experienced:

Conclusion

We can package our applications with a local SLM. This makes our applications cheaper, faster, connection-free, and self-contained.

Monday, January 13, 2025

Adding a UI to your WebAPI in ASP.NET 9.0

You may have noticed that when you create a WebAPI project in .NET 9.0, the default swagger UI is not there anymore by default. In this article, I will show you how to restore it back as the default UI. In addition, we will install an alternative UI to swagger named Scalar. The approach used applies to both minimal and controller-based WebAPI.

Companion Video: https://youtu.be/vsy-pIxKpYU
Source Code: https://github.com/medhatelmasry/WebApiDemo

Getting Started

Let us first create a minimal WebAPI project with the following terminal command:

dotnet new webapi -o WebApiDemo
cd WebApiDemo

Open the project in VS Code and note the following statements in Program.cs:

  • Around line 5 is this statement, which adds the OpenApi service:

builder.Services.AddOpenApi();

  • Also, around line 12 is this statement:

app.MapOpenApi();

The above statements produce the following JSON endpoint file describing your API when you run your app:

/openapi/v1.json

This displays a page like the following in your browser:

Restoring Swagger UI

Note that swagger UI is nowhere to be found. It is quite straight forward to add a Swagger UI to your .NET 9.0 WebAPI project. Start by adding the following package to your project:

dotnet add package Swashbuckle.AspNetCore

You can now add the following statement right under app.MapOpenApi():

app.UseSwaggerUI(options =>  {
    options.SwaggerEndpoint("/openapi/v1.json", "My WebAPI");
});

Now, run the project and go to endpoint /swagger in your browser. You should see this UI:

NOTE: If you want the Swagger UI to be accessed at the / (root) endpoint, then you can add this option:

options.RoutePrefix = "";

Scalar: alternative to Swagger

There is a much better alternative to swagger named Scalar. It offers you an enhanced UI and some additional features. Let us explore Scalar

To use Scalar in an ASP.NET 9.0 WebAPI application, you must add the following package:

dotnet add package Scalar.AspNetCore

Comment out the following code in Program.cs:

// app.UseSwaggerUI(options => 
// {
//     options.SwaggerEndpoint("/openapi/v1.json", "My WebAPI");
// });

Replace the above commented-out code with this:

app.MapScalarApiReference();

That's all you need to do. Let’s see how the Scalar UI looks like. Run the project and point your browser to this endpoint:

/scalar/v1

Explore this UI. One interesting feature is that you can view client code in a variety of languages. Here is what it looks like if you choose C#:


You can further customize the UI by enabling and disabling various options. For example, replace the statement app.MapScalarApiReference() with:

app.MapScalarApiReference(options => {
    options
        .WithTitle("My WebAPI")
        .WithTheme(ScalarTheme.Moon)
        .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient);
});

This results in a less dark theme and a default client code of C#.

Making Scalar UI the default page

To make the Scalar UI the default page when launching your WebAPI page with “dotnet watch”, edit the /Properties/launchSettings.json file and make the following changes:

1. In both http and https blocks, add this item:

"launchUrl": "scalar/v1"

2. Also, in both http and https blocks, change the value of launchBrowser to true.

Now when you restart your webAPI web app with “dotnet watch”, the Scalar UI is automatically loaded in your default browser.

Happy Coding!