How to Easily Create API Definitions Using Fern

How to Easily Create API Definitions Using Fern

Posted in

Building robust and well-documented APIs is essential for modern software development. However, the process of defining APIs and generating the necessary boilerplate code can be time-consuming and error-prone. Fortunately, Fern, is an open-source toolkit that can generate SDKs and documentation.

Through a powerful integration with FastAPI, Fern streamlines this process by automatically generating Pydantic models, exception classes, and networking logic based on API definitions. In this article, we’ll explore how to leverage Fern to easily create API definitions and generate the required code.

Getting Started

To get started with Fern, you’ll need to install the fern-api package globally using npm:

npm install -g fern-api

The fern/ Directory

The fern/ directory is the core of your API definition. It typically resides in your backend repository, although you can also have a dedicated repository for your API. To set up the directory structure, navigate to the root of your project and run the following command:

fern init

This command will create the following structure within your project:

fern/
├─ fern.config.json    # Root-level configuration
└─ api/                # Your API
   ├─ generators.yml    # Generators you're using
   └─ definition/

Already Have an OpenAPI Spec?

If you already have an existing OpenAPI specification, you can import it into Fern by running the following command:

fern init --openapi /path/to/openapi.yml
`

Generating an SDK

To generate a TypeScript SDK and an OpenAPI specification, execute the following command:

fern generate

By leveraging Fern’s code generation capabilities, you can save significant time and effort. Instead of manually writing FastAPI and Pydantic code, Fern generates it for you, ensuring consistency and accuracy. This automation eliminates the potential for human errors, such as typos or missing endpoints.

One of the significant advantages of using Fern’s auto-generated code is compile-time safety. Since the code is generated based on your API definition, any errors or inconsistencies will be caught during the compilation phase. For example, if you forget to define the getMovie endpoint in your API definition, attempting to compile the code will result in an error. This feature provides an extra layer of confidence that your API implementation is correct and aligned with the defined API structure.

Step 1: Define the API

The first step in utilizing Fern is to define your API using the Fern API definition language. This language allows you to specify service configurations, endpoints, types, and errors in a concise and structured manner. Let’s take a look at an example API definition for a movie API:

service:
  auth: false
  base-path: /movies
  endpoints:
    getMovie:
      method: GET
      path: /{movieId}
      path-parameters:
        movieId: MovieId
      response: Movie
      errors:
        - MovieDoesNotExistError

types:
  MovieId: string

  Movie:
    properties:
      id: MovieId
      title: string
      rating:
        type: double
        docs: The rating scale is one to five stars

  CreateMovieRequest:
    properties:
      title: string
      rating: double

errors:
  MovieDoesNotExistError:
    status-code: 404
    type: MovieId

In this example, we define the base path for the API as /movies and create an endpoint for retrieving movie details. We specify the method, path, path parameters, response type, and error handling.

Step 2: Run fern generate

Once you have defined your API using the Fern API definition language, you can use the fern generate command to automatically generate the boilerplate code. Fern analyzes your API definition and generates the necessary files and directories. Let’s run the command and see the generated code:

$ fern generate
[api]: fernapi/fern-fastapi-starter Downloaded to backend/src/fern_fastapi_starter/api/generated
┌─
│ ✓  fernapi/fern-fastapi-server
└─

After running fern generate, you can locate the auto-generated FastAPI and Pydantic code in the backend/src/fern_fastapi_starter/api/generated directory. Inside this directory, you’ll find the necessary files, including FastAPI routers, Pydantic models, exception classes, and other code that handles the networking and HTTP logic of your API. These generated files serve as the foundation for your API implementation.

Step 3: Implement the Server

With the generated code in place, you can focus solely on implementing the business logic of your API. Fern takes care of the networking and HTTP logic, allowing you to concentrate on defining the behavior of your endpoints. Let’s implement the movie API service:

# movie_service.py
from .api.generated.resources.imdb.service.service import AbstractImdbService
from .api.generated import Movie, MovieId

class MoviesService(AbstractImdbService):
    def get_movie(self, *, movie_id: str) -> Movie:
        return Movie(
            title="Goodwill Hunting",
            rating=9.8,
            id=MovieId.from_str("tt0119217"),
        )

# server.py
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from .api.generated.register import register
from .movies_service import MoviesService

app = FastAPI()

register(app, imdb=MoviesService())

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

def start() -> None:
    uvicorn.run(
        "src.fern_fastapi_starter.server:app",
        host="0.0.0.0",
        port=8080,
        reload=True,
    )

if __name__ == "__main__":
    start()

In this example, we create a MoviesService class that extends the AbstractMoviesService generated by Fern. We provide an implementation for the get_movie method, which retrieves movie details based on the provided movie_id. We also register the endpoints with FastAPI using the register function provided by Fern.

Step 4: Compile

To ensure the correctness of your API implementation, you can use a type checker like mypy to catch any potential errors in your code. Running the type checker ensures the code is valid and adheres to the defined API structure.

$ poetry run mypy
Success: no issues found in 24 source files

Step 5: Run the Server

Once you have successfully registered your endpoints and compiled your code, you can start the server and see your API in action. Running the server begins the FastAPI server, which handles the routing and HTTP requests, while Fern’s generated code performs the necessary logic based on your implemented business logic.

$ poetry run start
INFO:     Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
INFO:     Started reloader process [32816] using StatReload
INFO:     Started server process [32829]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

You can now use this like any other API. You can call it from any of your apps or websites, but for testing purposes we will use curl:

curl --location --request GET --silent 'localhost:8080/movies/goodwillhunting' | jq .

And here is a sample response:

paper@paper-System-Product-Name:~/GeekyHumans/fastapi-starter-main$ curl --location --request GET --silent 'localhost:8080/movies/goodwillhunting' | jq .
{
    "id": "tt0119217",
    "title": "Goodwill Hunting",
    "rating": 9.8
}

Final Words

Creating API definitions and generating the associated code can be a complex and time-consuming task. However, this process becomes significantly easier with Fern’s integration with FastAPI.

By defining your API using Fern’s API definition language, running the code generation command, implementing the business logic, and registering the endpoints with FastAPI, you can quickly develop robust APIs.

Fern automates the tedious parts, such as generating models, exceptions, and networking logic, allowing you to focus on building the core functionality of your API. With Fern, you can streamline the API development process and more easily create well-designed, documented, and efficient APIs.