NFT

In Sui, everything is an NFT - Objects are unique, non-fungible and owned. So technically, a simple type publishing is enough.

module examples::puppy {
    use std::string::String;
    use std::vector;

    use sui::object::{Self, ID, UID};
    use sui::tx_context::{Self, TxContext};
    use sui::event;

    /// An example NFT that can be minted by anybody. A Puppy is
    /// a freely-transferable object. Owner can add new traits to
    /// their puppy at any time and even change the image to the
    /// puppy's liking.
    struct Puppy has key, store {
        id: UID,
        /// Name of the Puppy
        name: String,
        /// Grumpy or chill?
        traits: vector<String>,
        /// The URL of the Puppy's image
        url: String,
    }

    /// Event: emitted when a new Puppy is minted.
    struct PuppyMinted has copy, drop {
        /// ID of the Puppy
        puppy_id: ID,
        /// The address of the NFT minter
        minted_by: address,
    }

    /// Mint a new Puppy with the given `name`, `traits` and `url`.
    /// The object is returned to sender and they're free to transfer
    /// it to themselves or anyone else.
    public fun mint(
        name: String,
        traits: vector<String>,
        url: String,
        ctx: &mut TxContext
    ): Puppy {
        let id = object::new(ctx);

        event::emit(PuppyMinted {
            puppy_id: object::uid_to_inner(&id),
            minted_by: tx_context::sender(ctx),
        });

        Puppy { id, name, traits, url }
    }

    /// Some puppies get new traits over time... owner of one can
    /// add a new trait to their puppy at any time.
    public fun add_trait(puppy: &mut Puppy, trait: String) {
        vector::push_back(&mut puppy.traits, trait);
    }

    /// As the puppy grows, owners can change the image to reflect
    /// the puppy's current state and look.
    public fun set_url(puppy: &mut Puppy, url: String) {
        puppy.url = url;
    }

    /// It's a good practice to allow the owner to destroy the NFT
    /// and get a storage rebate. Not a requirement and depends on
    /// your use case. At last, who doesn't love puppies?
    public fun destroy(puppy: Puppy) {
        let Puppy { id, url: _, name: _, traits: _ } = puppy;
        object::delete(id);
    }

    // Getters for properties.
    // Struct fields are always private and unless there's a getter,
    // other modules can't access them. It's up to the module author
    // to decide which fields to expose and which to keep private.

    /// Get the Puppy's `name`
    public fun name(puppy: &Puppy): String { puppy.name }

    /// Get the Puppy's `traits`
    public fun traits(puppy: &Puppy): &vector<String> { &puppy.traits }

    /// Get the Puppy's `url`
    public fun url(puppy: &Puppy): String { puppy.url }
}