Core Java

Introduction to Smithy

In software development, crafting APIs and services with clarity and precision is essential. Smithy, an open-source framework from AWS, delivers a powerful, declarative language for defining APIs and data models in a way that’s both human-readable and machine-processable. This article introduces the Smithy model and demonstrates how to implement a Java-based service using Smithy.

1. What Is Smithy?

Smithy is a declarative and flexible interface definition language (IDL) for defining service models. It offers a simple, expressive modeling language to describe the shapes (types), services, operations and protocols that make up an API, with models written in .smithy files that can be used to generate code, documentation, and other useful artifacts.

Smithy offers an expressive modeling language that is both easy to read and write, making it accessible not only to developers but also to non-technical stakeholders. Its extensibility allows for the use of custom traits and extensions, enabling users to adapt models to fit specific requirements.

In addition, Smithy provides powerful capabilities such as multi-language code generation (including Java), built-in validation to enforce best practices, and broad compatibility for generating both client and server code as well as documentation. These features make Smithy a versatile and efficient tool for modern API development.

2. Smithy Core Concepts

Smithy models are made up of several core concepts that define how APIs and their data structures are represented. These concepts form the foundation of any Smithy project and provide the building blocks for describing services in a consistent, language-neutral way. A Smithy model includes:

  • Shapes: Data types such as primitives, structures, unions, lists, maps, etc.
  • Resources: Domain entities with identifiers, operations, and child resources.
  • Services: Define available operations, protocols, metadata, and resource structure.
  • Operations: Define input/output types and possible errors.
  • Traits: Annotation-style metadata for constraints, documentation, protocols, etc.

File Extensions

Smithy models are typically stored in files with the .smithy extension. For example:

src/main/smithy/example/moviereview.smithy

Namespaces

Every Smithy model begins by declaring a namespace, which organises related resources, operations and services under a common scope. At the top of the file, we also specify the version of the Smithy IDL format being used. For example, we can define a namespace like this:

$version: "2"
namespace example.moviereview

Service Definition

In Smithy, a service definition represents the actual server that manages and interacts with data. Each service acts as a contract for how clients communicate with a backend system. We can define as many services as needed, with each one representing a different resource or domain to manage. A service is declared using the service keyword, followed by the name of the service.


/// Provides movie reviews.
service MovieReview {
    version: "2025-01-01"
    resources: [
        Movie
    ]
}

This service exposes a Movie resource, which represents a movie entity that clients can interact with.

Resource Definition

Resources in Smithy represent entities or collections of entities within a service. A resource maps real-world concepts such as users or orders into a structured API model. Each resource can include its identifiers, operations, and lifecycle, making it a core building block of service modeling. In simple terms, resources describe the data we will be working with. They are defined using the resource keyword followed by the name of the resource.

/// Represents a Movie resource.
resource Movie {
    identifiers: { movieId: MovieId }
    properties: {
        title: String
        director: String
        genre: String
    }
    read: GetMovie
    list: ListMovies
    operations: [
        AddReview
    ]
}

/// Pattern to validate Movie IDs (alphanumeric only).
@pattern("^[A-Za-z0-9 ]+$")
string MovieId

The resource keyword defines an entity that can be read, listed, and extended with operations. In this case, the Movie resource uses identifiers to uniquely identify each movie with a MovieId, while properties provide metadata such as title, director and genre. The read and list operations represent built-in resource lifecycle methods for retrieving one or multiple movies, and operations allow for custom actions beyond standard CRUD, such as AddReview.

The MovieId itself is defined as a custom string shape with a @pattern trait that ensures IDs only contain alphanumeric characters and spaces.

Defining Operations

Operations in Smithy define how a resource can be interacted with throughout its lifecycle. Standard lifecycle operations such as create, read, update, delete, and list provide canonical methods with well-defined semantics for accessing and transitioning the state of a resource, enabling automated tooling to better reason about the API.

Beyond these standard lifecycle methods, Smithy also allows defining custom operations through the operations property, which can be added to any resource or service shape to support actions that fall outside the typical CRUD and list patterns.

/// Operation to fetch a single movie by ID.
@readonly
operation GetMovie {
    input: GetMovieInput
    output: GetMovieOutput
}

/// Operation to list all movies.
@readonly
operation ListMovies {
    output: ListMoviesOutput
}

/// Operation to add a review to a movie.
operation AddReview {
    input: AddReviewInput
    output: AddReviewOutput
}

/// Input for GetMovie.
structure GetMovieInput {
    @required
    movieId: MovieId
}

/// Output for GetMovie.
structure GetMovieOutput {
    @required
    movieId: MovieId
    title: String
    description: String
    reviews: ReviewList
}

/// Output for ListMovies.
structure ListMoviesOutput {
    movies: MovieSummaryList
}

/// Input for AddReview.
structure AddReviewInput {
    @required
    movieId: MovieId
    reviewer: String
    comment: String
    rating: Integer
}

/// Output for AddReview.
structure AddReviewOutput {
    success: Boolean
}

/// Represents a summary of a movie.
structure MovieSummary {
    movieId: MovieId
    title: String
}

/// Represents a review entry.
structure Review {
    reviewer: String
    comment: String
    rating: Integer
}

list MovieSummaryList {
    member: MovieSummary
}

list ReviewList {
    member: Review
}

This Smithy definition introduces three operations for the Movie resource: GetMovie, ListMovies, and AddReview, each with corresponding input and output structures. The GetMovie operation retrieves a single movie by its movieId, returning details such as title, description, and a list of reviews.

Supporting structures such as MovieSummary, Review, and the lists MovieSummaryList and ReviewList organize the data, ensuring that movies and their reviews are represented consistently and can be expanded in the future.

3. Building the Smithy Model

We can build a Smithy model and generate additional artifacts using either the Smithy CLI or the Smithy Gradle Plugin. Building the model involves creating projections, applying plugins to generate artifacts, and running validation. If you are using the Smithy CLI, first ensure it is installed. Then, create a smithy-build.json file in the project directory:

{
    "version": "1.0",
    "sources": ["example"]
}

"version" ensures compatibility with the Smithy build specification, while "sources" defines the directory where your Smithy model files are found. Together, these properties guide Smithy on how to locate and process your service models during the build process.

Alternatively, if you are using Gradle, add the Smithy plugins and dependencies in your build.gradle file:

plugins {
    id 'java-library'
    id 'application'
    id 'software.amazon.smithy.gradle.smithy-jar' version '1.3.0'
    id 'software.amazon.smithy.gradle.smithy-base' version '1.3.0'
}

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation "software.amazon.smithy.java:aws-client-restjson:0.0.1"
    implementation "software.amazon.smithy.java:server-netty:0.0.1"
    implementation "software.amazon.smithy.java:aws-server-restjson:0.0.1"
    testImplementation libs.junit.jupiter
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

This setup enables Smithy’s Gradle plugin, which will parse .smithy files and generate code. Then, create a smithy-build.json file in the same directory:

{
    "version": "1.0",
    "sources": ["example"],
      "plugins": {
    "java-client-codegen": {
      "service": "com.jcg.example#MovieRecommendationService",
      "namespace": "com.jcg.example.movie",
      "protocol": "aws.protocols#restJson1"
    },
    "java-server-codegen": {
      "service": "com.jcg.example#MovieRecommendationService",
      "namespace": "com.jcg.example.movie"
    }
  }
}

This configuration instructs Smithy to generate Java code from the .smithy model and place the resulting classes under the com.jcg.example.movie package. Finally, run ./gradlew build to build the model.

Protocols

Before generating code from a Smithy model, we need to specify the protocol our API will use. Protocols define how the API communicates over the network, including serialization format, HTTP method, and URI structure. To do this, we tag the service definition with the desired protocol:

@aws.protocols#restJson1
service MovieService {
    version: "1.0",
    resources: [Movie]
}

Here, @aws.protocols#restJson1 indicates that the service will follow the REST JSON 1.1 protocol, which is widely used for RESTful APIs and is compatible with AWS code generation tools. You can now define the operations that will be exposed by your MovieService. For example, a simple read-only operation to fetch movie details can be described like this:

@readonly
@http(uri: "/movie/{movieId}", method: "GET")
operation GetMovie {
    input: GetMovieInput
    output: GetMovieOutput
}

In this example, @readonly indicates that the operation does not modify server data. @http specifies that the operation uses the GET method and is accessible via the /movie/{movieId} endpoint.

Implement the Server

After generating the server code from the Smithy file, the operations can be implemented. For example, the GetMovieOutput operation could be implemented like this:

public class MovieRecommendationServiceImpl implements MovieRecommendationService {
    @Override
    public GetMovieOutput getMovie(GetMovieInput input, RequestContext context) {
        return GetMovieOutput.builder()
            .movieId(input.movieId())
            .title("The Matrix")
            .description("A computer hacker discovers reality is a simulation.")
            .build();
     }
   }

Once the operations are implemented, an HTTP server can be created and started as follows:

Server server = Server.builder()
    .endpoints(URI.create("http://localhost:8000"))
    .addService(
        MovieRecommendationService.builder()
            .addRecommendMovieOperation(new RecommendMovieOperationImpl())
            .addListMoviesOperation(new ListMoviesOperationImpl())
            .build()
    )
    .build();

server.start();

This creates and starts an HTTP server on port 8000, registers the MovieRecommendationService, and wires in your RecommendMovieOperationImpl and ListMoviesOperationImpl.

Using the Java Client

Now that the server is running, let’s see how to interact with it using the generated Java client. The following example demonstrates how to call the service and retrieve a movie by its ID.

public class MovieClientExample {
    public static void main(String[] args) {
        // Create the client and point it to our running MovieRecommendationService
        MovieRecommendationServiceClient client = MovieRecommendationServiceClient.builder()
            .endpointResolver(EndpointResolver.staticEndpoint("http://localhost:8000"))
            .build();

        // Call the getMovie operation
        GetMovieOutput output = client.getMovie(GetMovieInput.builder()
            .movieId("m001")
            .build());

        // Print the result
        System.out.println("Movie ID: " + output.movie().movieId());
        System.out.println("Title: " + output.movie().title());
        System.out.println("Genre: " + output.movie().genre());
        System.out.println("Description: " + output.movie().description());
    }
}

This program creates a MovieRecommendationServiceClient connected to http://localhost:8000, the server we previously set up, and then sends a getMovie request using a specified movie ID.

4. Conclusion

In this article, we demonstrated how Smithy can be leveraged to model services and produce ready-to-use Java server and client code. Its approach simplifies API lifecycle management while ensuring maintainability and scalability.

This article provided an introduction to Smithy.

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button