Anatomy of a REST Server

This talk is about...

  • NOT networking / performance
  • NOT API design / data contracts
  • Code, structure, design patterns

separate business logic from transport

transport

business logic

backend

HTTP Basics

> POST /posts/ HTTP/1.1
> Host: jsonplaceholder.typicode.com
> User-Agent: curl/7.51.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 17
>
{
  "hello": "world"
}

Request

  • method
  • uri
  • headers
  • body
< HTTP/1.1 201 Created
< Date: Tue, 07 Feb 2017 23:21:59 GMT
< Content-Type: application/json; charset=utf-8
< Content-Length: 35
< Connection: keep-alive
< Set-Cookie: __cfduid=d699f6e2ba57404c619ee3ac866f4122b1486509718; expires=Wed, 07-Feb-18 23:21:58 GMT; path=/; domain=.typicode.com; HttpOnly
< X-Powered-By: Express
< Vary: Origin, X-HTTP-Method-Override, Accept-Encoding
< Access-Control-Allow-Credentials: true
< Cache-Control: no-cache
< Pragma: no-cache
< Expires: -1
< X-Content-Type-Options: nosniff
< Etag: W/"23-1zNmpcc43tiUIyNP55CUQQ"
< Via: 1.1 vegur
< Server: cloudflare-nginx
< CF-RAY: 32da94ce3b6e5017-DEN
<
{
  "hello": "world",
  "id": 101
}

Response

  • status
  • headers
  • body

client

server

req / res

Case API

elasticsearch

Transport

Routing

endpoint

handler

  • method
  • uri
  • request
  • response
  app.get '/user/:userId/tasks', TaskCtrl.activeTasks.fromHttp
  app.get '/tasks/:taskId', TaskCtrl.getTask.fromHttp
  app.post '/tasks/:taskId', TaskCtrl.upsert.fromHttp
  app.post '/tasks', TaskCtrl.upsert.fromHttp
  app.get '/user/:id/searchHistory', SearchHistoryCtrl.getRecentHistory.fromHttp
  app.get '/processingRate', ProcessingRateCtrl.getRate.fromHttp
  app.get '/dataProcessingTrend', DataProcessingTrendCtrl.getTrend.fromHttp
  app.post '/alarms/:alarmId/status', AlarmCtrl.updateStatus.fromHttp
  app.get '/alarms/:alarmId/comments', AlarmCtrl.getComments.fromHttp
  app.post '/alarms/:alarmId/comments', AlarmCtrl.addComments.fromHttp
  app.get '/alarms/:alarmId/properties', AlarmCtrl.getProperties.fromHttp
  app.get '/alarms/:alarmIds/smartResponse/:userId', AlarmCtrl.getAlarmSmartResponses.fromHttp
  app.get '/alarms/:alarmIds/smartResponse/:userId/:approve', AlarmCtrl.approveSmartResponseAction.fromHttp
  app.get '/searchesForUser', SearchCtrl.getSearchesForUser.fromHttp
  app.get '/searches/:searchId', SearchCtrl.getSearch.fromHttp
  app.get '/aieRules/:aieRuleId', AieRuleCtrl.getAieRule.fromHttp
  app.get '/logRhythmUser/:userId', UserCtrl.getUser.fromHttp
  app.post '/cases/collaborators/:caseId', CollaboratorCtrl.addCollaborationGroup.fromHttp
  app.post '/stats', StatCtrl.writeStat.fromHttp
  app.post '/graphql', GraphQLCtrl.graphql.fromHttp
private void bindRoutes() {
    threadPool(config.indexer.RequestThreads);
    port(config.indexer.HttpPort);
    post("/actions/zmq", (req, res) -> routeSearchRequest(req, res));
    post("/actions/dataUpdate", (req, res) -> routeDataUpdateRequest(req, res));
}
r := mux.NewRouter()
r.HandleFunc("/products/{key}", GetProduct).Methods("GET")
r.HandleFunc("/products/{key}", CreateProduct).Methods("POST")
r.HandleFunc("/products/{key}", RemoveProduct).Methods("DELETE")
r.HandleFunc("/articles/{category}/", GetArticles).Methods("GET")
r.HandleFunc("/articles/{category}/", CreateArticle).Methods("POST")
r.HandleFunc("/articles/{category}/{id:[0-9]+}", GetArticle).Methods("GET")

centralized routing

@Path(ESQueryResource.endpointName)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class ESQueryResource {

   protected static final String endpointName = "esquery";

   @POST
   public Response ESQuery() {
      return Response.status(200).build();
   }

   @GET
   public Response ListRepos() {
      return Response.status(200).build();
   }

}
namespace LogRhythm.Web.Services.ServicesHost.Controllers
{
    public class PingController : BaseController
    {
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public string Get()
        {
            if (log.IsInfoEnabled) log.Info(User.Identity.Name + ": Get");

            return DateTime.UtcNow.ToString();
        }

        public string Post()
        {
            if (log.IsInfoEnabled) log.Info(User.Identity.Name + ": Get");

            return DateTime.UtcNow.ToString();
        }
    }
}

decentralized routing

router.HandleFunc("/cases", func(res http.ResponseWriter, req *http.Request) {























}).Methods("POST")
router.HandleFunc("/cases", func(res http.ResponseWriter, req *http.Request) {
	contentType := req.Header.Get("content-type")

	if contentType != "application/json" {
		res.WriteHeader(415)
		return
	}

















}).Methods("POST")
router.HandleFunc("/cases", func(res http.ResponseWriter, req *http.Request) {
	contentType := req.Header.Get("content-type")

	if contentType != "application/json" {
		res.WriteHeader(415)
		return
	}

	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		res.WriteHeader(500)
		return
	}











}).Methods("POST")
router.HandleFunc("/cases", func(res http.ResponseWriter, req *http.Request) {
	contentType := req.Header.Get("content-type")

	if contentType != "application/json" {
		res.WriteHeader(415)
		return
	}

	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		res.WriteHeader(500)
		return
	}

	newCase := &CaseDetails{}
	err = json.Unmarshal(body, newCase)
	if err != nil {
		res.WriteHeader(400)
		return
	}




}).Methods("POST")
router.HandleFunc("/cases", func(res http.ResponseWriter, req *http.Request) {
	contentType := req.Header.Get("content-type")

	if contentType != "application/json" {
		res.WriteHeader(415)
		return
	}

	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		res.WriteHeader(500)
		return
	}

	newCase := &CaseDetails{}
	err = json.Unmarshal(body, newCase)
	if err != nil {
		res.WriteHeader(400)
		return
	}

	CaseController.CreateCase(newCase)

	res.WriteHeader(201)
}).Methods("POST")

handlers

Business Logic

  • native data structures
  • domain objects
  • business rules
type ChangeCaseStatusRequest struct {
	caseID    int
	newStatus int
}

func ChangeCaseStatus(req ChangeCaseStatusRequest) CaseDetails {
	caseInstance := backend.getCaseById(req.caseID)

	currentStatus := caseInstance.status

	if newStatus <= currentStatus {
		panic(APIError(400)
                    .Message("Cannot move backwards in the case workflow"))
	}

	caseInstance.status = req.newStatus

	backend.saveCase(caseInstance)
	return caseInstance
}

Backend

  • Another service
  • Database
  • File System
  • ...?

Request / Response

  • stateless
  • blocking
  • thread-per-request

My Server!

clients!

req / res threads

My Server!

req / res threads

error handling

authentication

content negotiation

deserialize input

validate input

ACTUAL BUSINESS LOGIC!

func catchErrors(res http.ResponseWriter) {
	err := recover()

	if err == nil {
		return
	}

        logger.Error("Error handling http request:\n", string(debug.Stack()))
 
	//check to see if it's a structured API error
	apiErr, ok := err.(*apiError)
	if !ok {
		apiErr = APIError(500)
		standardErr, ok := err.(error)
		if ok {
			apiErr.Details(standardErr.Error())
		}
	}

	//send the response to the client
	res.WriteHeader(apiErr.StatusCode)
	buffer, _ := json.Marshal(apiErr)
	res.Write(buffer)
	return
}

func (s *server) ServeHTTP(res http.ResponseWriter, req *http.Request) {
    defer catchErrors(res)
    router.ServeHTTP(res, req)
}
func ChangeCaseStatus(req ChangeCaseStatusRequest) CaseDetails {
	caseInstance, err := backend.getCaseById(req.caseID)
	if err != nil {
		panic(APIError(500).
			Message("Failed to retrieve case from database.").
			Details(err.Error()))
	}
	if caseInstance == nil {
		panic(APIError(404).
			Message("A case with id " + string(req.caseID) + " does not exist"))
	}

	currentStatus := caseInstance.status

	if newStatus <= currentStatus {
		panic(APIError(400).
			Message("Cannot move backwards in the case workflow"))
	}

	caseInstance.status = req.newStatus

	err = backend.saveCase(caseInstance)
	if caseInstance == nil {
		panic(APIError(500).
			Message("Failed to update case status.").
			Details(err.Error()))
	}
	return caseInstance
}
public BaseResponse searchIndex(Request req, Response res) {
    string body = req.body()
    try {
        Gson gson = new GsonBuilder().create();
        Envelope envelope = gson.fromJson(envelopeString, Envelope.class);
        return routeEnvelope(envelope);
    } catch (JsonSyntaxException e) {
        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setName("BadRequest");
        errorResponse.setStatusCode(400);
        errorResponse.setMessage("Invalid JSON: " + e.getMessage());
        errorResponse.setDetails(getStackTrace(e));
        return errorResponse;
    } catch (ApiException e) {
        ErrorResponse errorResponse = ErrorResponse.fromApiException(e);
        return errorResponse;
    } catch (Exception e) {
        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setName("ServerError");
        errorResponse.setStatusCode(500);
        errorResponse.setMessage("Unknown server error occurred: " + e.getMessage());
        errorResponse.setDetails(getStackTrace(e));
        return errorResponse;
    }
}

My Server!

req / res threads

shared resources & backpressure

DATA FILE

Solved Problems!

Content Negotiation

Cache Control

Made with Slides.com