DEV Community

guilhermegarcia86
guilhermegarcia86

Posted on • Edited on • Originally published at programadev.com.br

1

Spring Boot e MongoDB

O objetivo desse post é criar uma aplicação de geolocalização, dizer quais pontos estão próximos de você por exemplo isso é muito útil se você uma empresa de entregas e precisa saber qual fornecedor está mais próximo para a entrega e várias aplicações do gênero. Para isso usarei aqui Spring Boot, pois tem toda uma facilidade para criação do projeto e uma comunidade e documentações muito ativas, MongoDB além de ser uma boa opção por toda a flexibilidade que ele trás também é muito útil nesse cenário onde vamos precisar fazer cálculos de geolocalização e ele nativamente possui isso.
Aqui faremos só o backend da aplicação e futuramente desenvolveremos o frontend.

Criando a aplicação Spring Boot

Para isso vamos usar o Spring Initalizr, entrando na página escolhemos como queremos iniciar o projeto, aqui eu irei usar o Spring Web para poder fazer requisições Rest, também estou usando Lombok e Spring DevTools mas são mais pela facilidade que o Lombok fornece quando criarmos os nossos POJOs e o DevTools para podermos usar em desenvolvimento e termos o live reload da aplicação.
Então fica mais ou menos assim o projeto:
Spring Initializr

Após isso também precisamos adicionar ao projeto a dependência do google-service-maps, como estou usando Maven

<!-- https://mvnrepository.com/artifact/com.google.maps/google-maps-services -->
<dependency>
    <groupId>com.google.maps</groupId>
    <artifactId>google-maps-services</artifactId>
    <version>0.11.0</version>
</dependency>

Enter fullscreen mode Exit fullscreen mode

Também adicione o driver do Mongo ao pom.

<dependency>
    <groupId>org.mongodb</groupId>
    <artifactId>mongo-java-driver</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Descrição da aplicação

Vamos emular uma rede entregas de alimentos, onde os estabelecimentos estão cadastrados e quando um usuário digitar o seu endereço e ele irá exibir os mais próximos dele.

Model

Vamos começar o nosso model com o que seria então o Estabelecimento ele possuirá nome, email e a sua localização. Então vou começar criando a classe Localizacao.

package com.challenge.geolocation.model;

import java.util.List;

import lombok.Data;

@Data
public class Localizacao {

    private String endereco;
    private List<Double> coordinates;    
    private String type = "Point";

}
Enter fullscreen mode Exit fullscreen mode

Aqui temos o endereço mas também temos dois atributos que podem parecer um pouco estranhos o coordinates e o type. Os dois são necessários quando estamos trabalhando com geolocalização com o Mongo, o primeiro valor coordinates é uma lista de double contendo a latitude e longitude e o type diz respeito a um ponto no mapa, podemos ter outros types como Polygon.
Agora criando a nossa classe do estabelecimento propriamente dita.

package com.challenge.geolocation.model;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.mapping.Document;

import lombok.Data;

package com.challenge.geolocation.model;
import org.

import lombok.Data;

@Datapublic class Estabelecimento

    private ObjectId id;

    private String nome;
    private String email;
    private Localizacao localizacao;    

}

Enter fullscreen mode Exit fullscreen mode

Temos aqui a classe Estabelecimento composta pela classe Localizacao e com os atributos id lombok nos ajuda a reduzir um pouco a verbozidade.

Codecs

Agora temos o Model criado mas precisamos fazer de alguma forma pra que a nossa aplicação se comunique com o Mongo. Aí entra os codecs, ele vai ser o responsável por fazer tanto o envio como o recebimento dos objetos do Mongo.
Então vamos criar a classe EstabelecimentoCodec que implementa a interface CollectibleCodec do tipo Estabelecimento:

package com.challenge.geolocation.codec;

import org.bson.BsonReader;
import org.bson.BsonValue;
import org.bson.BsonWriter;
import org.bson.codecs.CollectibleCodec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;

import com.challenge.geolocation.model.Estabelecimento;

public class EstabelecimentoCodec implements CollectibleCodec<Estabelecimento>{

    @Override
    public void encode(BsonWriter writer, Estabelecimento value, EncoderContext encoderContext) {
        // TODO Auto-generated method stub

    }

    @Override
    public Class<Estabelecimento> getEncoderClass() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Estabelecimento decode(BsonReader reader, DecoderContext decoderContext) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Estabelecimento generateIdIfAbsentFromDocument(Estabelecimento document) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean documentHasId(Estabelecimento document) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public BsonValue getDocumentId(Estabelecimento document) {
        // TODO Auto-generated method stub
        return null;
    }

}
Enter fullscreen mode Exit fullscreen mode

Agora precisamos começar a implementar o codec a nossa maneira para ele poder fazer o encod e o decode, para isso vamos adicionar a classe Codec do pacote bson que nos ajuda, vamos tipa-la como um Document e vamos adicioná-lo ao construtor para ele ficar como dependência do nosso codec:

import org.bson.Document;
import org.bson.codecs.Codec;

import com.challenge.geolocation.model.Estabelecimento;


public class EstabelecimentoCodec implements CollectibleCodec<Estabelecimento>{

    private Codec<Document> codec;

    public EstabelecimentoCodec(Codec<Document> codec) {
        this.codec = codec;
    }
Enter fullscreen mode Exit fullscreen mode

Agora vamos implementar o método responsável por fazer o encode. Aqui é onde dizemos como serão salvo os nossos objetos em Java para um objeto do Mongo:

@Override
public void encode(BsonWriter writer, Estabelecimento estabelecimento, EncoderContext encoder) {
    Document document = new Document();

    document.put("_id", estabelecimento.getId());
    document.put("nome", estabelecimento.getNome());
    document.put("email", estabelecimento.getEmail());

    Localizacao localizacao = estabelecimento.getLocalizacao();

    List<Double> coordinates = new ArrayList<>();
    localizacao.getCoordinates().forEach(coordinates::add);

    document.put("localizacao", new Document()
            .append("endereco", localizacao.getEndereco())
            .append("coordinates", coordinates)
            .append("type", localizacao.getType()));

    codec.encode(writer, document, encoder);
}
Enter fullscreen mode Exit fullscreen mode

E aqui é onde impletamos o decode, como o Java vai interpretar o objeto retornado do Mongo:

@Override
public Estabelecimento decode(BsonReader reader, DecoderContext decoderContext) {

    Document document = codec.decode(reader, decoderContext);

    Estabelecimento estabelecimento = new Estabelecimento();
estabelecimento.
    estabelecimento.setNome(document.getString("nome"));
    estabelecimento.setEmail(document.getString("email"));

    Document localizacao = (Document) document.get("localizacao");
    if(localizacao != null) {
        String endereco = localizacao.getString("endereco");
        @SuppressWarnings("unchecked")
        List<Double> coordinates = (List<Double>) localizacao.get("coordinates");

        Localizacao localizacaoEntity = new Localizacao();
        localizacaoEntity.setEndereco(endereco);
        localizacaoEntity.setCoordinates(coordinates);

        estabelecimento.setLocalizacao(localizacaoEntity);
    }

    return estabelecimento;
}
Enter fullscreen mode Exit fullscreen mode

E temos os outros métodos que implementamos para que o codec consiga fazer a gerência dos objetos:

@Override
public Class<Estabelecimento> getEncoderClass() {
    return Estabelecimento.class;
}

@Override
public Estabelecimento generateIdIfAbsentFromDocument(Estabelecimento estabelecimento) {
    return documentHasId(estabelecimento) ? estabelecimento.generateId() : estabelecimento;
}

@Override
public boolean documentHasId(Estabelecimento estabelecimento) {
    return estabelecimento.getId() == null;
}

@Override
public BsonValue getDocumentId(Estabelecimento estabelecimento) {
    if (!documentHasId(estabelecimento)) {
        throw new IllegalStateException("This Document do not have a id");
    }

    return new BsonString(estabelecimento.getId().toHexString());
}
Enter fullscreen mode Exit fullscreen mode

A única coisa aqui a ressaltar foi a criação do método generateId no model Estabelecimento que fica assim:

package com.challenge.geolocation.model;
import org.

import lombok.Data;

@Datapublic class Estabelecimento

    private ObjectId id;

    private String nome;
    private String email;
    private Localizacao localizacao;    

    public Estabelecimento generateId() {
    this
        return this;
    }

}
Enter fullscreen mode Exit fullscreen mode

Repository

Agora temos o Model e o Codec agora podemos criar o nosso Repository que irá fazer o acesso ao banco e ficará responsável por toda a gerência no Mongo.

package com.challenge.geolocation.repository;

import org.springframework.stereotype.Repository;

import com.mongodb.MongoClient;
import com.mongodb.client.MongoDatabase;

@Repository
public class EstabelecimentoRepository {

    private MongoClient client;
    private MongoDatabase mongoDataBase;

Enter fullscreen mode Exit fullscreen mode

Aqui criei a classe EstabelecimentoRepository e utilizei a annotation @Repository que diz ao Spring que essa classe fará a administração com o banco, aqui já adicionei o MongoClient que fará o registro do Codec e fará a conexão ao banco e o MongoDatabase que é quem será responsável por nos trazer a instância onde poderemos buscar nas nossos collections e fazer as transações.
Agora vamos abrir a conexão com o banco de dados:

    @Value("${host}")
    private String host;

    @Value("${port}")
    private String port;

    @Value("${database}")
    private String database;

    @Value("${collection.estabelecimento}")
    private String estabelecimento;

    private MongoCollection<Estabelecimento> openConnetion() {
        Codec<Document> codec = MongoClient.getDefaultCodecRegistry().get(Document.class);

        EstabelecimentoCodec estCodec = new EstabelecimentoCodec(codec);

        CodecRegistry registry = CodecRegistries.fromRegistries(MongoClient.getDefaultCodecRegistry(),
                CodecRegistries.fromCodecs(estCodec));

        MongoClientOptions options = MongoClientOptions.builder().codecRegistry(registry).build();

        this.client = new MongoClient(host + ":" + port, options);
        this.mongoDataBase = client.getDatabase(database);

        return this.mongoDataBase.getCollection(this.estabelecimento, Estabelecimento.class);
    }

    private void closeConnection() {
        this.client.close();
    }
Enter fullscreen mode Exit fullscreen mode

Fazendo uso da annotation @Valeu do Spring conseguimos recuperar o valor que está no application.yml contendo o host, a port, o database e o nome da collection.
Então basicamente registramos o Codec e conectamos no Mongo e já pegamos a nossa collection e já deixamos pronto o método para fechar a conexão.
Agora vamos ao método que vai fazer a busca e agregação desses dados se baseando na proximidade.


    public List<Estabelecimento> searchByGeolocation(Filter filter) {
        try {
            MongoCollection<Estabelecimento> estabelecimentoCollection = openConnetion();

            estabelecimentoCollection.createIndex(Indexes.geo2dsphere("localizacao"));

            Point referencePoint = new Point(new Position(filter.getLat(), filter.getLng()));

            MongoCursor<Estabelecimento> resultados = estabelecimentoCollection
                    .find(Filters.nearSphere("localizacao", referencePoint, filter.getDistance(), 0.0)).limit(filter.getLimit()).iterator();

            List<Estabelecimento> estabelecimentos = fillEstabelecimento(resultados);

            return estabelecimentos;
        } finally {
            closeConnection();
        }
    }

    private List<Estabelecimento> fillEstabelecimento(MongoCursor<Estabelecimento> resultados) {
        List<Estabelecimento> estabelecimentos = new ArrayList<>();
        while (resultados.hasNext()) {
            estabelecimentos.add(resultados.next());
        }
        return estabelecimentos;
    }
Enter fullscreen mode Exit fullscreen mode

Então aqui abrimos a conexão com o método openConnetion() que nos devolve a nossa collection e como queremos fazer uma busca por proximidade adicionamos um índice e dizemos que o campo localizacao é do tipo 2dsphere se tivessemos usando Mongo ficaria assim:

db.estabelcimento.createIndex({
    localizacao : "2dsphere"
})
Enter fullscreen mode Exit fullscreen mode

Isso o que fizemos é o que o Mongo nos obriga a fazer se quisermos fazer a busca por geolocalização.
Em seguida criamos uma classe Point que recebe a latitude e a longitude e que será a baseado no nosso endereço quando passarmos. Como pegar a nossa latitude e longitude? Não se preocupe que iremos ver mais a seguir no momento só entenda que teremos esses valores pois é assim que poderemos trabalhar com geolocalização.
Agora podemos disparar a nossa pesquisa:

MongoCursor<Estabelecimento> resultados = estabelecimentoCollection
                    .find(Filters.nearSphere("localizacao", referencePoint, filter.getDistance(), 0.0)).limit(filter.getLimit()).iterator();
Enter fullscreen mode Exit fullscreen mode

Aqui está a nossa busca, temos a nossa collection e usamos o método find só que passamos dentro dele os nossos filtros para a agregação com a classe Filter e seu método estático nearSphere que ele recebe o campo onde ele vai fazer a busca que no caso é localizacao o ponto de referencia que é o nosso referencePoint que nós criamos com a latitude e longitude, a máxima distância, em metros, da nossa pesquisa e a mínima distância; também passamos o limit de resultados e chamamos o iterator para podermos percorrers o que nos voltar do Mongo.
Com um Iterator de resultados em mão podemos então percorrer o resultado, aqui separei no método fillEstabelecimento que devolve uma lista de Estabelecimento:

    private List<Estabelecimento> fillEstabelecimento(MongoCursor<Estabelecimento> resultados) {
        List<Estabelecimento> estabelecimentos = new ArrayList<>();
        while (resultados.hasNext()) {
            estabelecimentos.add(resultados.next());
        }
        return estabelecimentos;
    }
Enter fullscreen mode Exit fullscreen mode

Service

Agora faremos a classe de serviço, que irá executar a nossa consulta ao banco através do Repository e que irá ser chamada pela nossa Controller que nos passará o endereço e nós iremos fazer a transformação dele em latitude e longitude, para isso usaremos a dependência Google Maps mas antes disso teremos que nos registrar no GCP - Google Cloud Platform, para isso será necessário criar um conta lá, não se preocupe com isso pois o Google dará um valor em créditos caso seja seu primeiro registro e após isso não cobrará nada sem seu consentimento prévio, após criar a conta acesse o Console habilitar a API Geocoding API:

Geocoding API

Após isso é necessário criar uma Apikey para que a sua aplicação possa se comunicar com o serviço que foi habilitado:

Apikey

Agora a nossa apikey em mão já podmos começar a criar a classe EstabelecimentoService:

package com.challenge.geolocation.service;

import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.challenge.geolocation.dto.EstabelecimentoDTO;
import com.challenge.geolocation.filter.Filter;
import com.challenge.geolocation.model.Estabelecimento;
import com.challenge.geolocation.repository.EstabelecimentoRepository;
import com.google.maps.GeoApiContext;
import com.google.maps.GeocodingApi;
import com.google.maps.GeocodingApiRequest;
import com.google.maps.errors.ApiException;
import com.google.maps.model.GeocodingResult;
import com.google.maps.model.Geometry;
import com.google.maps.model.LatLng;

@Service
public class EstabelecimentoService {

    @Autowired
    private EstabelecimentoRepository repository;

    @Value("${apikey}")
    private String apikey;

    public List<EstabelecimentoDTO> procuraEstabelecimentosProximoAMim(String endereco, String distance, String limit) {

        GeoApiContext context = new GeoApiContext.Builder().apiKey(apikey).build();

        GeocodingApiRequest request = GeocodingApi.newRequest(context).address(endereco);

        try {
            GeocodingResult[] results = request.await();
            GeocodingResult resultado = results[0];

            Geometry geometry = resultado.geometry;

            LatLng location = geometry.location;

            List<Estabelecimento> estabelecimentoList = repository.searchByGeolocation(
                    Filter.toFilter(location.lat, location.lng, Double.valueOf(distance), Integer.valueOf(limit)));

            List<EstabelecimentoDTO> dtoList = estabelecimentoList.stream().map(estabelecimento -> {
                return EstabelecimentoDTO.toDTO(estabelecimento);
            }).collect(Collectors.toList());

            return dtoList;
        } catch (ApiException | InterruptedException | IOException e) {
            e.printStackTrace();
        }

        return List.of();
    }
}

Enter fullscreen mode Exit fullscreen mode

Criamos o método procuraEstabelecimentosProximoAMim que recebe o endereco, a distance e o limit, em seguida criamos GeoApiContext usando a nossa apikey fazemos a nossa requisição para a o serviço do Google passando o nosso endereço como estamos fazendo tudo isso de forma síncrona ficamos esperando o retorno do serviço externo, isso por si só pode ocasionar muitos problema então todo o método await lança Exceptions que aqui não vamos nos aprofundar tratando-as.
Após o retorno pegamos o resultado e navegamos no objeto de retorno até chegarmos onde queremos que é na location que é onde ele guarda a latitude e a longitude e agora podemos chamar o nosso Repository que irá executar a nossa busca.

Filter

Um ponto de observação, você deve ter notado a classe Filter e o método toFilter na nossa Service e na Repository o Filter com getLat, getLng, getDistance e getLimit. Ele nos ajuda a não passar um valor muito grande variáveis na chamada de um método, segue:

@Getter
public class Filter {

    private Filter() {}

    private double lat;
    private double lng;
    private double distance;
    private int limit;

    public static Filter toFilter(double latitude, double longitude, double distance, int limit) {
        Filter filter = new Filter();
        filter.lat = latitude;
        filter.lng = longitude;
        filter.distance = distance == 0 ? 1000.0 : distance;
        filter.limit = limit == 0 ? 10 : limit;     

        return filter;
    }

}
Enter fullscreen mode Exit fullscreen mode

Controller

Agora iremos criar a nossa Controller onde receberemos a requisição e devolveremos o resultado da pesquisa.

package com.challenge.geolocation.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.challenge.geolocation.dto.EstabelecimentoDTO;
import com.challenge.geolocation.service.EstabelecimentoService;

@RestController
@RequestMapping("api")
public class EstabalecimentoController {

    @Autowired
    private EstabelecimentoService service;

    @GetMapping("estabelecimento")
    public List<EstabelecimentoDTO> pegaEstabelecimentosProximosPeloEndereco(
            @RequestParam(name = "limit", defaultValue = "10") String limit,
            @RequestParam(name = "distancia", defaultValue = "1000.00") String distancia,
            @RequestParam("endereco") String endereco) {
        return service.procuraEstabelecimentosProximoAMim(endereco, distancia, limit);
    }

}
Enter fullscreen mode Exit fullscreen mode

Temos aqui a nossa EstabalecimentoController com unm método pegaEstabelecimentosProximosPeloEndereco e os parâmetros limit com um valor default de 10 caso não seja específicado na url, distancia com o valor de 1000.00, valor em metros e endereco.
Uma coisa interessante é que a nossa Controller não devolve o nosso Model pois não é considerado uma boa prática e até mesmo um falha de segurança dependedo da situação então o que devolvemos? Devolvemos um DTO (Data Transfer Object) que é um objeto POJO que só trafega dados como no exemplo:

package com.challenge.geolocation.dto;

import com.challenge.geolocation.model.Estabelecimento;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;

import lombok.Getter;

@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
@Getter
public class EstabelecimentoDTO {

    private EstabelecimentoDTO() {}

    private String nome;
    private String email;
    private String endereco;

    public static EstabelecimentoDTO toDTO(Estabelecimento estabelecimento) {
        EstabelecimentoDTO dto = new EstabelecimentoDTO();

        dto.nome = estabelecimento.getNome();
        dto.email = estabelecimento.getEmail();
        dto.endereco = estabelecimento.getLocalizacao().getEndereco();      

        return dto;
    }
}
Enter fullscreen mode Exit fullscreen mode

A única responsabilidade desse DTO é transformar um Estabelecimento em um DTO para ser retornado para a Controller e isso é feito dentro da nossa classe de Service no retorno da nossa Repository.

Resultado final

Antes de testarmos precisamos fazer a inclusão de dados, então podemos inserir no Mongo os dados:

{
    "nome" : "Mercado I",
    "email" : "contato@mercadoI.com",
    "localizacao" : {
        "endereco" : "Rua Brigadeiro Tobias n 780",
        "coordinates" : [ 
            -23.53624, 
            -46.63395
        ],
        "type" : "Point"
    }
}

{    
    "nome" : "Estabelecimento II",
    "email" : "contato@mercadoII.com",
    "localizacao" : {
        "endereco" : "R. Brg. Tobias, 206 - Santa Ifigênia, São Paulo - SP, 01032-000",
        "coordinates" : [ 
            -23.54165, 
            -46.63583
        ],
        "type" : "Point"
    }
}

{    
    "nome" : "Estabelecimento III",
    "email" : "contato@mercadoIII.com",
    "localizacao" : {
        "endereco" : "Av. Cásper Líbero, 42 - Centro Histórico De São Paulo, São Paulo - SP, 01033-000",
        "coordinates" : [ 
            -23.54132, 
            -46.63643
        ],
        "type" : "Point"
    }
}

{    
    "nome" : "Estabelecimento IV",
    "email" : "contato@mercadoIV.com",
    "localizacao" : {
        "endereco" : "Av. Rio Branco, 630 - República, São Paulo - SP, 01205-000",
        "coordinates" : [ 
            -23.53984, 
            -46.64008
        ],
        "type" : "Point"
    }
}

{    
    "nome" : "Estabelecimento V",
    "email" : "contato@mercadoV.com",
    "localizacao" : {
        "endereco" : "Alameda Barão de Limeira, 425 - Campos Elíseos, São Paulo - SP, 01202-900",
        "coordinates" : [ 
            -23.53386, 
            -46.6482
        ],
        "type" : "Point"
    }
}

{    
    "nome" : "Estabelecimento VI",
    "email" : "contato@mercadoVI.com",
    "localizacao" : {
        "endereco" : "R. Canuto do Val, 41 - Santa Cecilia, São Paulo - SP, 01224-040",
        "coordinates" : [ 
            -23.54062, 
            -46.65114
        ],
        "type" : "Point"
    }
}

Enter fullscreen mode Exit fullscreen mode

Agora vamos fazer um teste manual usando o Insomnia um client para requisições HTTP, mas você usar qualquer um de sua prefência, PostMan, PostWoman, cUrl e etc.

Teste PostMan

Então fazendo a requisição http://localhost:8080/api/estabelecimento?endereco=R. Brg. Tobias, 247
Temos a reposta:

[
  {
    "nome": "Estabelecimento II",
    "email": "contato@mercadoII.com",
    "endereco": "R. Brg. Tobias, 206 - Santa Ifigênia, São Paulo - SP, 01032-000"
  },
  {
    "nome": "Estabelecimento III",
    "email": "contato@mercadoIII.com",
    "endereco": "Av. Cásper Líbero, 42 - Centro Histórico De São Paulo, São Paulo - SP, 01033-000"
  },
  {
    "nome": "Mercado I",
    "email": "contato@mercadoI.com",
    "endereco": "Rua Brigadeiro Tobias n 780"
  },
  {
    "nome": "Estabelecimento IV",
    "email": "contato@mercadoIV.com",
    "endereco": "Av. Rio Branco, 630 - República, São Paulo - SP, 01205-000"
  }
]
Enter fullscreen mode Exit fullscreen mode

Projeto Final

O projeto final você pode encontrar aqui aqui

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay