Implement a Twitter clone. Write a simple web service in Go that has the following API endpoints:
- Create a user.
- List all users.
- Get user profile by ID.
- Create a tweet.
- List all tweets.
- Get tweet by ID.
I have used an architecture following a mix of principles lined up in the Onion, Clean and Hexagonal Architecture and Domain-driven design (DDD). I have included the Repository Pattern to abstract the data layer from the business logic.
The packages worth mentioning are:
-
internal/service/repository/postgres/orm: the "database-first" ORM auto generated code from the database schema that SQLBoiler generates. -
There are 2 repositories:
internal/service/repository/postgres: the repository that implements the interfaces defined ininternal/service/repository/interfaces.gointernal/service/repository/memory: a memory mock repository that implements the interfaces defined ininternal/service/repositoryinterfaces.go. This is used for unit testing.
To interact with this repositories, there is a Store. This store can chain multiple repository operations in a single transaction. It sort of follows the same principles as in the Unit of Work pattern. There is a in-memory store and a persistent (postgres) store. The in-memory store is used for unit testing. The service layer uses the store to interact with the repositories. It never interacts directly with the repositories.
The business logic is implemented in the internal/service package. This represents the use cases of the application - the domain service. Note that the business logic always use data entities defined in the internal/entities package. This ensures business logic is decoupled from the data layer defined in the internal/service/repository package.
The use cases are exposed via the API layer. The API layer is implemented using the go-chi router.
The endpoints routes were generated from a Open-API spec file using oapi-codegen. The code generated is in internal/api/v1/openapi.
The following endpoints are exposed:
# Get the API spec
GET /api/v1/api.json
# Get all tweets
GET /api/v1/tweets
# Create a tweet
POST /api/v1/tweets
# Get a tweet by id
GET /api/v1/tweets/{id}
# Create a user
POST /api/v1/users
# Get all users
GET /api/v1/users
# Get a user by id
GET /api/v1/users/{id}- docker
- docker-compose
Ideally, the application should be launched from the makefile. This makes sure docker-compose is run with the correct environment variables. Otherwise, set the environment variables defined in the env-template file. See env-template for the instructions.
To start the application, run:
make docker-upIt runs in attached mode, so you can see the logs of the application.
Below are a few examples of how to use the API. Note that the API spec is available at the endpoint /api/v1/api.json.
## Create a user
curl -v -X POST -H "Content-Type: application/json" \
-d '{ "username": "foo", "name": "John Doe", "email": "jd@mail.com" }' \
http://localhost:8888/api/v1/users
## Get all users
curl -v http://localhost:8888/api/v1/users
## Set user_id as an environment variable
user_id=$(curl http://localhost:8888/api/v1/users | jq -r '.[0].id')
## Get a user by id
curl -v http://localhost:8888/api/v1/users/$user_id
## Create a tweet
curl -v -X POST -H "Content-Type: application/json" \
-d '{"user_id":"'$user_id'", "content": "Hello World!" }' \
http://localhost:8888/api/v1/tweets
# Get all tweets
curl -v http://localhost:8888/api/v1/tweets
## Set twitter_id as an environment variable
tweet_id=$(curl http://localhost:8888/api/v1/tweets | jq -r '.[0].id')
## Get a tweet by id
curl -v http://localhost:8888/api/v1/tweets/$tweet_idWhen you are done, stop the application in another terminal with:
make docker-downThe make help command lists all the available targets.
# Start / stop the database running in docker
db-start Postgres start
db-stop Postgres stop
# Open a Postgres CLI
db-cli Start the Postgres CLI
# Manage the database schema
db-migrate-down Run database downgrade the last migration
db-migrate-up Run database upgrade migrations
db-migrate-version Print the current migration version
# Generate code
openapi-generate Generate OpenAPI client
db-orm-models Generate Go database models
# Development targets
dev Run development server
lint Lint and format source code based on golangci configuration
# Runs tests
test Run unit tests
test_integration Run integration tests
test_e2e Run end-to-end tests
# Starts the API in docker (starts the database and runs the migrations if needed)
api-start Run docker API container
api-stop Stop docker API container
# Runs docker-compose up/down with the correct environment variables
docker-down Stop docker container
docker-up Run docker containerIf you don't have a postgres database running locally, you can start one with:
make db-startIf you have one, edit the .env file (copied from the env-template) to point to your database.
To update the database schema, run the migrations with:
make db-migrate-upEdit the code at your convenience.
Launch the API with:
make devYou can stop it with Ctrl-C.
Run the tests with:
make test
make test_integrationRun the linter with:
make lintRun the end-to-end tests with:
make test_e2eWhen you are done, stop the database with:
make db-stop- Add pagination to the API
- Probably use Cursor Pagination
- Change the API spec (V2?) to use cursor pagination
- The return values of the API should be wrapped in a
datafield (see jsonapi), namely fetching multiple resources.
- The return values of the API should be wrapped in a